├── .github └── workflows │ └── python-app.yml ├── .gitignore ├── CommonWidget.py ├── DDMonitor.spec ├── DDMonitor_macos.spec ├── DDMonitor_unix.spec ├── DD监控室.py ├── LICENSE ├── LayoutConfig.py ├── LayoutPanel.py ├── LiverSelect.py ├── QR └── Alipay.png ├── README.md ├── ReportException.py ├── VideoWidget.py ├── VideoWidget_vlc.py ├── danmu.py ├── docs ├── live-api.md ├── vlc-player.md └── vlc-plugins.md ├── favicon.ico ├── hooks └── hook-vlc.py ├── log.py ├── pay.py ├── remote.py ├── requirements.txt ├── scripts ├── build_linux.sh ├── build_macos.sh ├── build_win.bat ├── run.bat ├── run.sh └── sign_macos.sh └── utils ├── ascii.txt ├── danmu.png ├── entitlements.plist ├── help.html ├── qdark.qss ├── splash.jpg └── vtb.csv /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | name: DDMonitor Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-linux: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 3.8 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.8 16 | - name: Update pip 17 | run: python -m pip install --upgrade pip 18 | - name: Install Packages 19 | run: pip install -r requirements.txt 20 | - name: Build 21 | run: "scripts/build_linux.sh" 22 | - name: Archive Build 23 | uses: actions/upload-artifact@v2 24 | with: 25 | name: ddmonitor-linux-build 26 | path: dist/DDMonitor 27 | 28 | build-windows: 29 | 30 | runs-on: windows-latest 31 | env: 32 | PYTHON_VLC_MODULE_PATH: ${{github.workspace}}\\vlc-3.0.12 33 | PYTHON_VLC_LIB_PATH: ${{github.workspace}}\\vlc-3.0.12\\libvlc.dll 34 | steps: 35 | - uses: actions/checkout@v2 36 | - uses: suisei-cn/actions-download-file@v1 37 | id: downloadVLC 38 | name: Download VLC 39 | with: 40 | url: "https://download.videolan.org/pub/videolan/vlc/last/win64/vlc-3.0.12-win64.zip" 41 | target: . 42 | - name: Extract VLC 43 | run: | 44 | powershell.exe Expand-Archive -Path vlc-3.0.12-win64.zip -DestinationPath . 45 | - name: Set up Python 3.8 46 | uses: actions/setup-python@v2 47 | with: 48 | python-version: 3.8 49 | - name: Update pip 50 | run: python -m pip install --upgrade pip 51 | - name: Install Packages 52 | run: pip install -r requirements.txt 53 | - name: Build 54 | run: "scripts/build_win.bat" 55 | - name: Archive Build 56 | uses: actions/upload-artifact@v2 57 | with: 58 | name: ddmonitor-windows-build 59 | path: dist/DDMonitor 60 | 61 | build-macos: 62 | 63 | runs-on: macos-latest 64 | 65 | steps: 66 | - uses: actions/checkout@v2 67 | - name: Set up Python 3.8 68 | uses: actions/setup-python@v2 69 | with: 70 | python-version: 3.8 71 | - name: Update pip 72 | run: python -m pip install --upgrade pip 73 | - name: Install Packages 74 | run: pip install -r requirements.txt 75 | - name: Build 76 | run: "scripts/build_macos.sh" 77 | - name: Archive Build 78 | uses: actions/upload-artifact@v2 79 | with: 80 | name: ddmonitor-macos-build 81 | path: dist/DDMonitor 82 | 83 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE setting 2 | .DS_Store 3 | .vscode/ 4 | .idea/ 5 | venv/ 6 | 7 | # Cache & binary & logs 8 | __pycache__ 9 | *.pyo 10 | cache/ 11 | build/ 12 | dist/ 13 | *.build/ 14 | *.dist/ 15 | logs/ 16 | 17 | # Config 18 | config_*.json 19 | config.json 20 | !utils/config_default.json 21 | 22 | # VLC bin 23 | plugins/ 24 | *.dll 25 | -------------------------------------------------------------------------------- /CommonWidget.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """一些公用的组件 3 | """ 4 | from PyQt5.QtCore import Qt, pyqtSignal 5 | from PyQt5.QtWidgets import QSlider 6 | 7 | 8 | class Slider(QSlider): 9 | """通用的滚动条""" 10 | value = pyqtSignal(int) 11 | 12 | def __init__(self, value=100): 13 | super(Slider, self).__init__() 14 | self.setOrientation(Qt.Horizontal) 15 | self.setFixedWidth(100) 16 | self.setValue(value) 17 | 18 | def mousePressEvent(self, event): 19 | self.updateValue(event.pos()) 20 | 21 | def mouseMoveEvent(self, event): 22 | self.updateValue(event.pos()) 23 | 24 | def wheelEvent(self, event): # 把进度条的滚轮事件去了 用啥子滚轮 25 | pass 26 | 27 | def updateValue(self, QPoint): 28 | value = QPoint.x() 29 | if value > 100: value = 100 30 | elif value < 0: value = 0 31 | self.setValue(value) 32 | self.value.emit(value) 33 | -------------------------------------------------------------------------------- /DDMonitor.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | # PyQt5 exclude modules 6 | pyqt_exc = [ 7 | 'PyQt5.QtNetwork', 8 | 'PyQt5.QtQml', 9 | 'PyQt5.QAxContainer', 10 | 'PyQt5.QtBluetooth', 11 | 'PyQt5.QtDBus', 12 | 'PyQt5.QtDesigner', 13 | 'PyQt5.QtHelp', 14 | 'PyQt5.QtMultimedia', 15 | 'PyQt5.QtMultimediaWidgets', 16 | 'PyQt5.QtNetworkAuth', 17 | 'PyQt5.QtNfc', 18 | 'PyQt5.QtOpenGL', 19 | 'PyQt5.QtPositioning', 20 | 'PyQt5.QtLocation', 21 | 'PyQt5.QtPrintSupport', 22 | 'PyQt5.QtQuick', 23 | 'PyQt5.QtQuick3D', 24 | 'PyQt5.QtQuickWidgets', 25 | 'PyQt5.QtRemoteObjects', 26 | 'PyQt5.QtSensors', 27 | 'PyQt5.QtSerialPort', 28 | 'PyQt5.QtSql', 29 | 'PyQt5.QtSvg', 30 | 'PyQt5.QtTest', 31 | 'PyQt5.QtTextToSpeech', 32 | 'PyQt5.QtWebChannel', 33 | 'PyQt5.QtWebSockets', 34 | 'PyQt5.QtWinExtras', 35 | 'PyQt5.QtXml', 36 | 'PyQt5.QtXmlPatterns' 37 | ] 38 | 39 | 40 | a = Analysis(['DD监控室.py'], 41 | pathex=[], 42 | binaries=[], 43 | datas=[ 44 | ('utils/ascii.txt', '.'), 45 | ('utils/help.html', '.'), 46 | ('utils/qdark.qss', 'utils'), 47 | ('utils/splash.jpg', 'utils'), 48 | ('utils/vtb.csv', 'utils'), 49 | ('scripts/run.bat', '.'), 50 | ], 51 | hiddenimports=[], 52 | hookspath=['hooks'], 53 | runtime_hooks=[], 54 | excludes=pyqt_exc, 55 | win_no_prefer_redirects=False, 56 | win_private_assemblies=False, 57 | cipher=block_cipher, 58 | noarchive=False) 59 | pyz = PYZ(a.pure, 60 | a.zipped_data, 61 | cipher=block_cipher) 62 | exe = EXE(pyz, 63 | a.scripts, 64 | [], 65 | exclude_binaries=True, 66 | name='DDMonitor', 67 | debug=False, 68 | bootloader_ignore_signals=False, 69 | strip=False, 70 | upx=False, 71 | icon='favicon.ico', 72 | console=False ) 73 | coll = COLLECT(exe, 74 | a.binaries, 75 | a.zipfiles, 76 | a.datas, 77 | strip=False, 78 | upx=False, 79 | upx_exclude=[], 80 | name='DDMonitor') 81 | -------------------------------------------------------------------------------- /DDMonitor_macos.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | a = Analysis(['DD监控室.py'], 6 | pathex=[], 7 | binaries=[ 8 | ], 9 | datas=[ 10 | ('utils/ascii.txt', '.'), 11 | ('utils/help.html', '.'), 12 | ('utils/qdark.qss', 'utils'), 13 | ('utils/splash.jpg', 'utils'), 14 | ('utils/vtb.csv', 'utils'), 15 | ], 16 | hiddenimports=[], 17 | hookspath=[], 18 | runtime_hooks=[], 19 | excludes=[ 20 | # PyQt5 21 | 'PyQt5.QtNetwork', 22 | 'PyQt5.QtQml', 23 | 'PyQt5.QAxContainer', 24 | 'PyQt5.QtBluetooth', 25 | 'PyQt5.QtDBus', 26 | 'PyQt5.QtDesigner', 27 | 'PyQt5.QtHelp', 28 | 'PyQt5.QtMultimedia', 29 | 'PyQt5.QtMultimediaWidgets', 30 | 'PyQt5.QtNetworkAuth', 31 | 'PyQt5.QtNfc', 32 | 'PyQt5.QtOpenGL', 33 | 'PyQt5.QtPositioning', 34 | 'PyQt5.QtLocation', 35 | 'PyQt5.QtPrintSupport', 36 | 'PyQt5.QtQuick', 37 | 'PyQt5.QtQuick3D', 38 | 'PyQt5.QtQuickWidgets', 39 | 'PyQt5.QtRemoteObjects', 40 | 'PyQt5.QtSensors', 41 | 'PyQt5.QtSerialPort', 42 | 'PyQt5.QtSql', 43 | 'PyQt5.QtSvg', 44 | 'PyQt5.QtTest', 45 | 'PyQt5.QtTextToSpeech', 46 | 'PyQt5.QtWebChannel', 47 | 'PyQt5.QtWebSockets', 48 | 'PyQt5.QtWinExtras', 49 | 'PyQt5.QtXml', 50 | 'PyQt5.QtXmlPatterns' 51 | ], 52 | win_no_prefer_redirects=False, 53 | win_private_assemblies=False, 54 | cipher=block_cipher, 55 | noarchive=False) 56 | pyz = PYZ(a.pure, a.zipped_data, 57 | cipher=block_cipher) 58 | exe = EXE(pyz, 59 | a.scripts, 60 | [], 61 | exclude_binaries=True, 62 | name='DDMonitor', 63 | debug=False, 64 | bootloader_ignore_signals=False, 65 | strip=False, 66 | upx=False, 67 | icon='favicon.ico', 68 | console=False ) 69 | 70 | coll = COLLECT(exe, 71 | a.binaries, 72 | a.zipfiles, 73 | a.datas, 74 | strip=False, 75 | upx=False, 76 | name='DDMonitor') 77 | 78 | app = BUNDLE(coll, 79 | name='DDMonitor.app', 80 | icon='favicon.ico', 81 | bundle_identifier='com.github.zhimingshenjun.ddmonitor', 82 | info_plist={ 83 | 'NSAppleScriptEnabled': False, 84 | 'NSPrincipalClass': 'NSApplication', 85 | 'NSAppleScriptEnabled': False, 86 | 'CFBundleDocumentTypes': [], 87 | 'CFBundleName': 'DDMonitor' 88 | } 89 | ) 90 | -------------------------------------------------------------------------------- /DDMonitor_unix.spec: -------------------------------------------------------------------------------- 1 | # -*- mode: python ; coding: utf-8 -*- 2 | 3 | block_cipher = None 4 | 5 | a = Analysis(['DD监控室.py'], 6 | pathex=[], 7 | binaries=[ 8 | ], 9 | datas=[ 10 | ('utils/ascii.txt', '.'), 11 | ('utils/help.html', '.'), 12 | ('utils/qdark.qss', 'utils'), 13 | ('utils/splash.jpg', 'utils'), 14 | ('utils/vtb.csv', 'utils'), 15 | ('scripts/run.sh', '.'), 16 | ], 17 | hiddenimports=[], 18 | hookspath=[], 19 | runtime_hooks=[], 20 | excludes=[ 21 | # PyQt5 22 | 'PyQt5.QtNetwork', 23 | 'PyQt5.QtQml', 24 | 'PyQt5.QAxContainer', 25 | 'PyQt5.QtBluetooth', 26 | 'PyQt5.QtDBus', 27 | 'PyQt5.QtDesigner', 28 | 'PyQt5.QtHelp', 29 | 'PyQt5.QtMultimedia', 30 | 'PyQt5.QtMultimediaWidgets', 31 | 'PyQt5.QtNetworkAuth', 32 | 'PyQt5.QtNfc', 33 | 'PyQt5.QtOpenGL', 34 | 'PyQt5.QtPositioning', 35 | 'PyQt5.QtLocation', 36 | 'PyQt5.QtPrintSupport', 37 | 'PyQt5.QtQuick', 38 | 'PyQt5.QtQuick3D', 39 | 'PyQt5.QtQuickWidgets', 40 | 'PyQt5.QtRemoteObjects', 41 | 'PyQt5.QtSensors', 42 | 'PyQt5.QtSerialPort', 43 | 'PyQt5.QtSql', 44 | 'PyQt5.QtSvg', 45 | 'PyQt5.QtTest', 46 | 'PyQt5.QtTextToSpeech', 47 | 'PyQt5.QtWebChannel', 48 | 'PyQt5.QtWebSockets', 49 | 'PyQt5.QtWinExtras', 50 | 'PyQt5.QtXml', 51 | 'PyQt5.QtXmlPatterns' 52 | ], 53 | win_no_prefer_redirects=False, 54 | win_private_assemblies=False, 55 | cipher=block_cipher, 56 | noarchive=False) 57 | pyz = PYZ(a.pure, a.zipped_data, 58 | cipher=block_cipher) 59 | exe = EXE(pyz, 60 | a.scripts, 61 | [], 62 | exclude_binaries=True, 63 | name='DDMonitor', 64 | debug=False, 65 | bootloader_ignore_signals=False, 66 | strip=False, 67 | upx=True, 68 | icon='favicon.ico', 69 | console=True ) 70 | coll = COLLECT(exe, 71 | a.binaries, 72 | a.zipfiles, 73 | a.datas, 74 | strip=False, 75 | upx=True, 76 | upx_exclude=[], 77 | name='DDMonitor') 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 489 | USA 490 | 491 | Also add information on how to contact you by electronic and paper mail. 492 | 493 | You should also get your employer (if you work as a programmer) or your 494 | school, if any, to sign a "copyright disclaimer" for the library, if 495 | necessary. Here is a sample; alter the names: 496 | 497 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 498 | library `Frob' (a library for tweaking knobs) written by James Random 499 | Hacker. 500 | 501 | , 1 April 1990 502 | Ty Coon, President of Vice 503 | 504 | That's all there is to it! 505 | -------------------------------------------------------------------------------- /LayoutConfig.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 布局方案常量 3 | ''' 4 | 5 | layoutList = [ 6 | [(0, 0, 1, 1)], [(0, 0, 1, 1), (0, 1, 1, 1)], [(0, 0, 1, 1), (1, 0, 1, 1)], 7 | [(0, 0, 1, 1), (1, 0, 1, 1), (2, 0, 1, 1)], [(0, 0, 1, 1), (0, 1, 1, 1), (0, 2, 1, 1)], 8 | [(0, 0, 2, 2), (0, 2, 1, 1), (1, 2, 1, 1)], [(0, 0, 2, 2), (2, 0, 1, 1), (2, 1, 1, 1)], 9 | [(0, 0, 1, 1), (0, 1, 1, 1), (1, 0, 1, 1), (1, 1, 1, 1)], 10 | [(0, 0, 1, 1), (1, 0, 1, 1), (2, 0, 1, 1), (3, 0, 1, 1)], 11 | [(0, 0, 3, 3), (0, 3, 1, 1), (1, 3, 1, 1), (2, 3, 1, 1)], 12 | [(0, 0, 3, 3), (3, 0, 1, 1), (3, 1, 1, 1), (3, 2, 1, 1)], 13 | [(0, 0, 1, 1), (0, 1, 1, 1), (0, 2, 1, 1), (1, 0, 1, 1), (1, 1, 1, 1), (1, 2, 1, 1)], 14 | [(0, 0, 2, 2), (0, 2, 1, 1), (1, 2, 1, 1), (2, 0, 1, 1), (2, 1, 1, 1), (2, 2, 1, 1)], 15 | [(0, 0, 1, 1), (0, 1, 1, 1), (0, 2, 1, 1), (0, 3, 1, 1), (1, 0, 1, 1), (1, 1, 1, 1), (1, 2, 1, 1), (1, 3, 1, 1)], 16 | [(0, 0, 3, 3), (0, 3, 1, 1), (1, 3, 1, 1), (2, 3, 1, 1), (3, 0, 1, 1), (3, 1, 1, 1), (3, 2, 1, 1), (3, 3, 1, 1)], 17 | [(0, 0, 1, 1), (0, 1, 1, 1), (0, 2, 1, 1), (1, 0, 1, 1), (1, 1, 1, 1), (1, 2, 1, 1), (2, 0, 1, 1), (2, 1, 1, 1), (2, 2, 1, 1)], 18 | [(0, 0, 2, 2), (0, 2, 1, 1), (0, 3, 1, 1), (1, 2, 1, 1), (1, 3, 1, 1), 19 | (2, 0, 2, 2), (2, 2, 1, 1), (2, 3, 1, 1), (3, 2, 1, 1), (3, 3, 1, 1)], 20 | [(0, 0, 1, 1), (0, 1, 1, 1), (0, 2, 1, 1), (0, 3, 1, 1), (1, 0, 1, 1), (1, 1, 1, 1), (1, 2, 1, 1), (1, 3, 1, 1), 21 | (2, 0, 1, 1), (2, 1, 1, 1), (2, 2, 1, 1), (2, 3, 1, 1)], 22 | [(0, 0, 1, 1), (0, 1, 1, 1), (0, 2, 1, 1), (0, 3, 1, 1), (0, 4, 1, 1), 23 | (1, 0, 1, 1), (1, 1, 1, 1), (1, 2, 1, 1), (1, 3, 1, 1), (1, 4, 1, 1), 24 | (2, 0, 1, 1), (2, 1, 1, 1), (2, 2, 1, 1), (2, 3, 1, 1), (2, 4, 1, 1)], 25 | [(0, 0, 1, 1), (0, 1, 1, 1), (0, 2, 1, 1), (0, 3, 1, 1), 26 | (1, 0, 1, 1), (1, 1, 1, 1), (1, 2, 1, 1), (1, 3, 1, 1), 27 | (2, 0, 1, 1), (2, 1, 1, 1), (2, 2, 1, 1), (2, 3, 1, 1), 28 | (3, 0, 1, 1), (3, 1, 1, 1), (3, 2, 1, 1), (3, 3, 1, 1)], 29 | ] -------------------------------------------------------------------------------- /LayoutPanel.py: -------------------------------------------------------------------------------- 1 | """ 2 | 选择布局方式的页面 3 | """ 4 | from PyQt5.QtWidgets import QLabel, QWidget, QGridLayout 5 | from PyQt5.QtGui import QFont 6 | from PyQt5.QtCore import Qt, pyqtSignal 7 | from LayoutConfig import layoutList 8 | 9 | 10 | class Label(QLabel): 11 | """序号标签。用于布局的编号""" 12 | def __init__(self, text): 13 | super(Label, self).__init__() 14 | self.setText(text) 15 | self.setFont(QFont('微软雅黑', 13, QFont.Bold)) 16 | self.setAlignment(Qt.AlignCenter) 17 | self.setStyleSheet('background-color:#4682B4') 18 | 19 | 20 | class LayoutWidget(QLabel): 21 | """布局表示 22 | 展示一种布局 23 | """ 24 | clicked = pyqtSignal(int) 25 | 26 | def __init__(self, layout, number): 27 | super(LayoutWidget, self).__init__() 28 | self.number = number # 布局编号 29 | mainLayout = QGridLayout(self) 30 | for index, rect in enumerate(layout): 31 | y, x, h, w = rect 32 | mainLayout.addWidget(Label(str(index + 1)), y, x, h, w) 33 | 34 | def mousePressEvent(self, QMouseEvent): 35 | self.clicked.emit(self.number) 36 | 37 | def enterEvent(self, QEvent): 38 | self.setStyleSheet('background-color:#AFEEEE') 39 | 40 | def leaveEvent(self, QEvent): 41 | self.setStyleSheet('background-color:#00000000') 42 | 43 | 44 | class LayoutSettingPanel(QWidget): 45 | """布局选择窗口""" 46 | layoutConfig = pyqtSignal(list) 47 | 48 | def __init__(self): 49 | super(LayoutSettingPanel, self).__init__() 50 | self.resize(1280, 720) 51 | self.setWindowTitle('选择布局方式') 52 | 53 | # 排列各种布局方式 54 | mainLayout = QGridLayout(self) 55 | mainLayout.setSpacing(15) 56 | mainLayout.setContentsMargins(15, 15, 15, 15) 57 | layoutWidgetList = [] 58 | for index, layout in enumerate(layoutList): 59 | widget = LayoutWidget(layout, index) 60 | widget.clicked.connect(self.sendLayout) 61 | mainLayout.addWidget(widget, index // 4, index % 4) 62 | layoutWidgetList.append(widget) 63 | 64 | def sendLayout(self, number): 65 | self.layoutConfig.emit(layoutList[number]) 66 | self.hide() 67 | -------------------------------------------------------------------------------- /LiverSelect.py: -------------------------------------------------------------------------------- 1 | """ 2 | DD监控室主界面上方的控制条里的ScrollArea里面的卡片模块 3 | 包含主播开播/下播检测和刷新展示 置顶排序 录制管理等功能 4 | """ 5 | import requests, json, time, codecs, logging, os 6 | from PyQt5.QtWidgets import * # QAction,QFileDialog 7 | from PyQt5.QtGui import * # QIcon,QPixmap 8 | from PyQt5.QtCore import * # QSize 9 | 10 | 11 | header = { 12 | 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' 13 | } 14 | 15 | 16 | class CardLabel(QLabel): 17 | def __init__(self, text='NA', fontColor='#f1fefb', size=11): 18 | super(CardLabel, self).__init__() 19 | self.setFont(QFont('微软雅黑', size, QFont.Bold)) 20 | self.setStyleSheet('color:%s;background-color:#00000000' % fontColor) 21 | self.setText(text) 22 | 23 | def setBrush(self, fontColor): 24 | self.setStyleSheet('color:%s;background-color:#00000000' % fontColor) 25 | 26 | 27 | class OutlinedLabel(QLabel): 28 | def __init__(self, text='NA', fontColor='#FFFFFF', outColor='#222222', size=11): 29 | super().__init__() 30 | self.setFont(QFont('微软雅黑', size, QFont.Bold)) 31 | self.setStyleSheet('background-color:#00000000') 32 | self.setText(text) 33 | self.setBrush(fontColor) 34 | self.setPen(outColor) 35 | self.w = self.font().pointSize() / 15 36 | self.metrics = QFontMetrics(self.font()) 37 | 38 | def setBrush(self, brush): 39 | brush = QColor(brush) 40 | if not isinstance(brush, QBrush): 41 | brush = QBrush(brush) 42 | self.brush = brush 43 | 44 | def setPen(self, pen): 45 | pen = QColor(pen) 46 | if not isinstance(pen, QPen): 47 | pen = QPen(pen) 48 | pen.setJoinStyle(Qt.RoundJoin) 49 | self.pen = pen 50 | 51 | def paintEvent(self, event): 52 | rect = self.rect() 53 | indent = self.indent() 54 | x = rect.left() + indent - min(self.metrics.leftBearing(self.text()[0]), 0) 55 | y = (rect.height() + self.metrics.ascent() - self.metrics.descent()) / 2 56 | path = QPainterPath() 57 | path.addText(x, y, self.font(), self.text()) 58 | qp = QPainter(self) 59 | qp.setRenderHint(QPainter.Antialiasing) 60 | self.pen.setWidthF(self.w * 2) 61 | qp.strokePath(path, self.pen) 62 | qp.fillPath(path, self.brush) 63 | 64 | 65 | class CircleImage(QWidget): 66 | """圆形头像框""" 67 | def __init__(self, parent=None): 68 | super(CircleImage, self).__init__(parent) 69 | self.setFixedSize(60, 60) 70 | self.circle_image = None 71 | 72 | def set_image(self, image): 73 | self.circle_image = image 74 | self.update() 75 | 76 | def paintEvent(self, event): 77 | if self.circle_image: 78 | painter = QPainter(self) 79 | painter.setRenderHint(QPainter.Antialiasing, True) 80 | pen = Qt.NoPen 81 | painter.setPen(pen) 82 | brush = QBrush(self.circle_image) 83 | painter.setBrush(brush) 84 | painter.drawRoundedRect(self.rect(), self.width() / 2, self.height() / 2) 85 | 86 | 87 | class PushButton(QPushButton): 88 | def __init__(self, name, pushToken=False): 89 | super().__init__() 90 | self.setText(name) 91 | self.pushToken = pushToken 92 | if self.pushToken: 93 | self.setStyleSheet('background-color:#3daee9;border-width:1px') 94 | else: 95 | self.setStyleSheet('background-color:#31363b;border-width:1px') 96 | 97 | 98 | # class RequestAPI(QThread): 99 | # data = pyqtSignal(dict) 100 | # 101 | # def __init__(self, roomID): 102 | # super(RequestAPI, self).__init__() 103 | # self.roomID = roomID 104 | # 105 | # def run(self): 106 | # r = requests.get(r'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=%s' % self.roomID) 107 | # self.data.emit(json.loads(r.text)) 108 | 109 | 110 | class RecordThread(QThread): 111 | """获取直播推流并录制 112 | TODO: 换用 bilibili_api.live.get_room_play_url(room_id) 113 | """ 114 | downloadTimer = pyqtSignal(str) 115 | downloadError = pyqtSignal(str) 116 | 117 | def __init__(self, roomID): 118 | super(RecordThread, self).__init__() 119 | self.roomID = roomID 120 | self.recordToken = False 121 | self.downloadToken = False 122 | self.downloadTime = 0 # s 123 | self.checkTimer = QTimer() 124 | self.checkTimer.timeout.connect(self.checkDownlods) 125 | self.reconnectCount = 0 126 | 127 | def checkDownlods(self): 128 | if self.downloadToken: 129 | self.downloadToken = False 130 | if not self.downloadTime % 60: # 每分钟刷新一次 131 | self.downloadTimer.emit('%dmin' % (self.downloadTime / 60)) 132 | self.downloadTime += 3 133 | else: 134 | self.reconnectCount += 1 135 | if self.reconnectCount > 60: # 60 x 3s = 180s重试 超时了就退出 136 | self.downloadError.emit(self.roomID) 137 | 138 | def setSavePath(self, savePath): 139 | self.savePath = savePath 140 | 141 | def run(self): 142 | self.reconnectCount = 0 143 | api = r'https://api.live.bilibili.com/room/v1/Room/playUrl?cid=%s&platform=web&qn=10000' % self.roomID 144 | try: 145 | r = requests.get(api) 146 | url = json.loads(r.text)['data']['durl'][0]['url'] 147 | download = requests.get(url, stream=True, headers=header) 148 | self.recordToken = True 149 | self.downloadTime = 0 # 初始化下载时间为0s 150 | self.cacheVideo = open(self.savePath, 'wb') 151 | for chunk in download.iter_content(chunk_size=512): 152 | if not self.recordToken: 153 | break 154 | if chunk: 155 | self.downloadToken = True 156 | self.cacheVideo.write(chunk) 157 | self.cacheVideo.close() 158 | except: 159 | logging.exception("下载视频到缓存失败") 160 | 161 | 162 | class DownloadImage(QThread): 163 | """下载图片""" 164 | img = pyqtSignal(QPixmap) 165 | img_origin = pyqtSignal(QPixmap) 166 | 167 | def __init__(self, scaleW, scaleH, keyFrame=False): 168 | super(DownloadImage, self).__init__() 169 | self.W = scaleW 170 | self.H = scaleH 171 | self.keyFrame = keyFrame 172 | 173 | def setUrl(self, url): 174 | self.url = url 175 | 176 | def run(self): 177 | try: 178 | if self.W == 60: 179 | r = requests.get(self.url + '@100w_100h.jpg') 180 | else: 181 | r = requests.get(self.url) 182 | img = QPixmap.fromImage(QImage.fromData(r.content)) 183 | self.img.emit(img.scaled(self.W, self.H, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) 184 | if self.keyFrame: 185 | self.img_origin.emit(img) 186 | except Exception as e: 187 | logging.error(str(e)) 188 | 189 | 190 | class CoverLabel(QLabel): 191 | """封面的文字""" 192 | addToWindow = pyqtSignal(list) 193 | deleteCover = pyqtSignal(str) 194 | changeTopToken = pyqtSignal(list) 195 | 196 | def __init__(self, roomID, topToken=False): 197 | super(CoverLabel, self).__init__() 198 | QToolTip.setFont(QFont('微软雅黑', 16, QFont.Bold)) 199 | self.setAcceptDrops(True) 200 | self.roomID = roomID 201 | self.topToken = topToken 202 | self.isPlaying = False # 正在播放 203 | self.title = 'NA' # 这里其实一开始设计的时候写错名字了 实际这里是用户名不是房间号 将错就错下去了 204 | self.roomTitle = '' # 这里才是真的存放房间名的地方 205 | self.recordState = 0 # 0 无录制任务 1 录制中 2 等待开播录制 206 | self.savePath = '' 207 | self.setFixedSize(160, 90) 208 | self.setObjectName('cover') 209 | self.setFrameShape(QFrame.Box) 210 | self.firstUpdateToken = True 211 | self.layout = QGridLayout(self) 212 | self.profile = CircleImage() 213 | self.layout.addWidget(self.profile, 0, 4, 2, 2) 214 | if topToken: 215 | brush = '#FFC125' 216 | self.setStyleSheet('#cover{border-width:3px;border-style:solid;border-color:#dfa616;background-color:#5a636d}') 217 | else: 218 | brush = '#f1fefb' 219 | self.setStyleSheet('background-color:#5a636d') # 灰色背景 220 | self.titleLabel = OutlinedLabel(fontColor=brush) 221 | # self.titleLabel = CardLabel(fontColor=brush) 222 | self.layout.addWidget(self.titleLabel, 0, 0, 1, 6) 223 | # self.roomIDLabel = OutlinedLabel(roomID, fontColor=brush) 224 | # self.roomIDLabel = CardLabel(roomID, fontColor=brush) 225 | # self.layout.addWidget(self.roomIDLabel, 1, 0, 1, 6) 226 | self.stateLabel = OutlinedLabel(size=13) 227 | # self.stateLabel = CardLabel(size=13) 228 | self.stateLabel.setText('检测中') 229 | self.liveState = 0 # 0 未开播 1 直播中 2 投稿视频 -1 错误 230 | self.layout.addWidget(self.stateLabel, 1, 0, 1, 6) 231 | self.downloadFace = DownloadImage(60, 60) 232 | self.downloadFace.img.connect(self.updateProfile) 233 | self.downloadKeyFrame = DownloadImage(160, 90, True) 234 | self.downloadKeyFrame.img.connect(self.updateKeyFrame) 235 | self.downloadKeyFrame.img_origin.connect(self.setToolTipKeyFrame) 236 | 237 | self.recordThread = RecordThread(roomID) 238 | self.recordThread.downloadTimer.connect(self.refreshStateLabel) 239 | self.recordThread.downloadError.connect(self.recordError) 240 | 241 | def updateLabel(self, info): 242 | if not info[0]: # 用户或直播间不存在 243 | self.liveState = -1 244 | self.roomTitle = '' 245 | self.setToolTip(self.roomTitle) 246 | if info[2]: 247 | self.titleLabel.setText(info[2]) 248 | self.stateLabel.setText('房间可能被封') 249 | else: 250 | self.titleLabel.setText(info[1]) 251 | self.stateLabel.setText('无该房间或已加密') 252 | self.setStyleSheet('background-color:#8B3A3A') # 红色背景 253 | else: 254 | if self.firstUpdateToken: # 初始化 255 | self.firstUpdateToken = False 256 | self.downloadFace.setUrl(info[3]) # 启动下载头像线程 257 | self.downloadFace.start() 258 | # self.roomIDLabel.setText(info[1]) # 房间号 259 | self.titleLabel.setText(info[2]) # 名字 260 | self.title = info[2] 261 | if info[4] == 1: # 直播中 262 | self.liveState = 1 263 | self.downloadKeyFrame.setUrl(info[5]) # 启动下载关键帧线程 264 | self.downloadKeyFrame.start() 265 | self.roomTitle = info[6] # 房间直播标题 266 | # self.setToolTip(self.roomTitle) # 改用self.setToolTipKeyFrame里面设置tooltip 267 | else: # 未开播 268 | self.liveState = 0 269 | self.roomTitle = '' # 房间直播标题 270 | self.setToolTip(self.roomTitle) 271 | self.clear() 272 | if self.isPlaying: 273 | self.setStyleSheet('#cover{border-width:3px;border-style:solid;border-color:red;background-color:#5a636d}') 274 | elif self.topToken: 275 | self.setStyleSheet('#cover{border-width:3px;border-style:solid;border-color:#dfa616;background-color:#5a636d}') 276 | else: 277 | self.setStyleSheet('background-color:#5a636d') # 灰色背景 278 | self.refreshStateLabel() 279 | 280 | def refreshStateLabel(self, downloadTime=''): 281 | if self.liveState == 1: 282 | if self.recordState == 1: # 录制中 283 | self.stateLabel.setBrush('#87CEFA') # 录制中为蓝色字体 284 | if downloadTime: 285 | self.stateLabel.setText('· 录制中 %s' % downloadTime) 286 | else: 287 | self.stateLabel.setBrush('#7FFFD4') # 直播中为绿色字体 288 | self.stateLabel.setText('· 直播中') 289 | else: 290 | if self.recordState == 2: # 等待录制 291 | self.stateLabel.setBrush('#FFA500') # 待录制为橙色字体 292 | self.stateLabel.setText('· 等待开播') 293 | else: 294 | self.stateLabel.setBrush('#FF6A6A') # 未开播为红色字体 295 | self.stateLabel.setText('· 未开播') 296 | 297 | def recordError(self, roomID): 298 | self.recordThread.checkTimer.stop() 299 | self.refreshStateLabel() 300 | QMessageBox.information(self, '录制中止', '%s %s 录制结束 请检查网络或主播是否掉线' % (self.title, roomID), QMessageBox.Ok) 301 | 302 | def updateProfile(self, img): 303 | self.profile.set_image(img) 304 | 305 | def updateKeyFrame(self, img): 306 | self.setPixmap(img) 307 | 308 | def setToolTipKeyFrame(self, img): 309 | buffer = QBuffer() 310 | buffer.open(QIODevice.WriteOnly) 311 | img.save(buffer, "PNG", quality=100) 312 | image = bytes(buffer.data().toBase64()).decode() 313 | html = ''.format(image) 314 | self.setToolTip('
%s

%s
' % (self.roomTitle.strip(), html)) 315 | 316 | def dragEnterEvent(self, QDragEnterEvent): 317 | QDragEnterEvent.acceptProposedAction() 318 | 319 | def mousePressEvent(self, QMouseEvent): # 设置drag事件 发送拖动封面的房间号 320 | if QMouseEvent.button() == Qt.LeftButton: 321 | drag = QDrag(self) 322 | mimeData = QMimeData() 323 | mimeData.setText('roomID:%s' % self.roomID) 324 | drag.setMimeData(mimeData) 325 | drag.exec_() 326 | elif QMouseEvent.button() == Qt.RightButton: 327 | menu = QMenu() 328 | addTo = menu.addMenu('添加至窗口 ►') 329 | addWindow = [] 330 | for win in range(1, 10): 331 | addWindow.append(addTo.addAction('窗口%s' % win)) 332 | if not self.topToken: 333 | top = menu.addAction('添加置顶') 334 | else: 335 | top = menu.addAction('取消置顶') 336 | record = None 337 | if self.recordState == 0: # 无录制任务 338 | if self.liveState == 1: 339 | record = menu.addAction('录制(最高画质)') 340 | elif self.liveState in [0, 2]: # 未开播或轮播 341 | record = menu.addAction('开播自动录制') 342 | else: # 录制中或等待录制 343 | record = menu.addAction('取消录制') 344 | openBrowser = menu.addAction('打开直播间') 345 | copyRoomID = menu.addAction('复制房号 %s' % self.roomID) 346 | menu.addSeparator() # 添加分割线,防止误操作 347 | delete = menu.addAction('删除') 348 | action = menu.exec_(self.mapToGlobal(QMouseEvent.pos())) 349 | if action == delete: 350 | self.deleteCover.emit(self.roomID) 351 | self.roomID = '0' 352 | self.hide() 353 | elif action == top: 354 | if self.topToken: 355 | self.titleLabel.setBrush('#f1fefb') 356 | # self.roomIDLabel.setBrush('#f1fefb') 357 | if self.isPlaying: 358 | self.setStyleSheet('#cover{border-width:3px;border-style:solid;border-color:red;background-color:#5a636d}') 359 | else: 360 | self.setStyleSheet('border-width:0px') 361 | else: 362 | self.titleLabel.setBrush('#FFC125') 363 | # self.roomIDLabel.setBrush('#FFC125') 364 | if self.isPlaying: 365 | self.setStyleSheet('#cover{border-width:3px;border-style:solid;border-color:red;background-color:#5a636d}') 366 | else: 367 | self.setStyleSheet('#cover{border-width:3px;border-style:solid;border-color:#dfa616;background-color:#5a636d}') 368 | self.topToken = not self.topToken 369 | self.changeTopToken.emit([self.roomID, self.topToken]) # 发送修改后的置顶token 370 | elif action == record: 371 | if self.roomID != '0': 372 | if self.recordState == 0: # 无录制任务 373 | saveName = '%s_%s_%s' % (self.title, self.roomTitle, 374 | time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time()))) 375 | self.savePath = QFileDialog.getSaveFileName(self, "选择保存路径", saveName, "*.flv")[0] 376 | if self.savePath: # 保存路径有效 377 | if self.liveState == 1: # 直播中 378 | self.recordThread.setSavePath(self.savePath) 379 | self.recordThread.start() 380 | self.recordThread.checkTimer.start(3000) 381 | self.recordState = 1 # 改为录制状态 382 | self.refreshStateLabel('0min') 383 | elif self.liveState in [0, 2]: # 未开播或轮播中 384 | self.recordState = 2 # 改为等待录制状态 385 | self.refreshStateLabel() 386 | elif self.recordState == 1: # 录制中→取消录制 387 | self.recordState = 0 # 取消录制 388 | self.recordThread.checkTimer.stop() 389 | self.recordThread.recordToken = False # 设置录像线程标志位让它自行退出结束 390 | self.refreshStateLabel() 391 | elif self.recordState == 2: # 等待录制→取消录制 392 | self.recordState = 0 # 取消录制 393 | self.recordThread.checkTimer.stop() 394 | self.refreshStateLabel() 395 | elif action == openBrowser: 396 | if self.roomID != '0': 397 | QDesktopServices.openUrl(QUrl(r'https://live.bilibili.com/%s' % self.roomID)) 398 | elif action == copyRoomID: 399 | clipboard = QApplication.clipboard() 400 | clipboard.setText(self.roomID) 401 | else: 402 | for index, i in enumerate(addWindow): 403 | print(index, i) 404 | if action == i: 405 | self.addToWindow.emit([index, self.roomID]) # 添加至窗口 窗口 房号 406 | break 407 | 408 | 409 | class GetHotLiver(QThread): 410 | """获取指定分区的 热榜""" 411 | roomInfoSummary = pyqtSignal(list) 412 | 413 | def __init__(self): 414 | super(GetHotLiver, self).__init__() 415 | 416 | def run(self): 417 | try: 418 | roomInfoSummary = [] 419 | for area in [9, 2, 3, 6, 1]: 420 | pageSummary = [] 421 | for p in range(1, 6): 422 | api = 'https://api.live.bilibili.com/xlive/web-interface/v1/second/getList?platform=web&parent_area_id=%s&page=%s' % (area, p) 423 | r = requests.get(api) 424 | data = json.loads(r.text)['data']['list'] 425 | if data: 426 | for info in data: 427 | pageSummary.append([info['uname'], info['title'], str(info['roomid'])]) 428 | time.sleep(0.1) 429 | roomInfoSummary.append(pageSummary) 430 | if roomInfoSummary: 431 | self.roomInfoSummary.emit(roomInfoSummary) 432 | except: 433 | logging.exception('房间信息获取失败') 434 | 435 | 436 | class GetFollows(QThread): 437 | """获取指定用户的关注列表 438 | TODO: 换用 439 | + 获取关注列表:bilibili_api.user.get_followings_g(uid) 440 | + 获取直播间地址:bilibili_api.user.get_live_info(uid) 441 | """ 442 | roomInfoSummary = pyqtSignal(list) 443 | 444 | def __init__(self): 445 | super(GetFollows, self).__init__() 446 | self.uid = None 447 | 448 | def setUID(self, uid): 449 | self.uid = uid 450 | 451 | def run(self): 452 | if self.uid: 453 | followsIDs = set() 454 | roomIDList = [] 455 | burp0_headers = {"Connection": "close", 456 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.68", 457 | "DNT": "1", "Accept": "*/*", "Sec-Fetch-Site": "same-site", 458 | "Sec-Fetch-Mode": "no-cors", "Sec-Fetch-Dest": "script", 459 | "Referer": "https://space.bilibili.com/", "Accept-Encoding": "gzip, deflate", 460 | "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"} 461 | for p in range(1, 6): 462 | urls = [f"https://api.bilibili.com:443/x/relation/followings?vmid={self.uid}&pn={p}&ps=50&order=desc&jsonp=jsonp", 463 | f"https://api.bilibili.com:443/x/relation/followings?vmid={self.uid}&pn={p}&ps=50&order=asc&jsonp=jsonp"] 464 | for burp0_url in urls: 465 | r = requests.get(burp0_url, headers=burp0_headers) 466 | followList = json.loads(r.text)['data']['list'] 467 | if followList: 468 | for i in followList: 469 | followsIDs.add(i['mid']) 470 | followsIDs = list(followsIDs) 471 | if followsIDs: 472 | data = json.dumps({'uids': followsIDs}) 473 | r = requests.post(r'https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids', data=data) 474 | r.encoding = 'utf8' 475 | data = json.loads(r.text)['data'] 476 | for followID in followsIDs: 477 | for uid, info in data.items(): 478 | if uid == str(followID): 479 | roomIDList.append([info['uname'], info['title'], str(info['room_id'])]) 480 | break 481 | else: 482 | logging.error('未获取到有效房号列表或房号列表为空') 483 | if roomIDList: 484 | self.roomInfoSummary.emit(roomIDList) 485 | 486 | 487 | class DownloadVTBList(QThread): 488 | """更新 VTB 信息""" 489 | vtbList = pyqtSignal(list) 490 | 491 | def __init__(self, parent=None): 492 | super(DownloadVTBList, self).__init__(parent) 493 | 494 | def run(self): 495 | try: 496 | headers = { 497 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36'} 498 | r = requests.get(r'https://github.com/zhimingshenjun/DD_Monitor/blob/master/utils/vtb.csv', headers=headers) 499 | # r.encoding = 'utf8' 500 | vtbList = [] 501 | html = r.text.split('\n') 502 | for cnt, line in enumerate(html): 503 | if 'blob-num js-line-number' in line: 504 | vtbID = html[cnt + 1].split('>')[1].split('<')[0] 505 | roomID = html[cnt + 2].split('>')[1].split('<')[0] 506 | haco = html[cnt + 3].split('>')[1].split('<')[0] 507 | vtbList.append('%s,%s,%s\n' % (vtbID, roomID, haco)) 508 | if vtbList: 509 | self.vtbList.emit(vtbList) 510 | except: 511 | logging.exception("vtbs 列表获取失败") 512 | 513 | 514 | class HotLiverTable(QTableWidget): 515 | """关注列表""" 516 | addToWindow = pyqtSignal(list) 517 | 518 | def __init__(self): 519 | super().__init__() 520 | 521 | def contextMenuEvent(self, event): 522 | self.menu = QMenu(self) 523 | addTo = self.menu.addMenu('添加至窗口 ►') 524 | addWindow = [] 525 | for win in range(1, 10): 526 | addWindow.append(addTo.addAction('窗口%s' % win)) 527 | action = self.menu.exec_(self.mapToGlobal(event.pos())) 528 | for index, i in enumerate(addWindow): 529 | if action == i: 530 | text=self.item(self.currentRow(), 2).text() 531 | self.addToWindow.emit([index, text]) 532 | break 533 | 534 | 535 | class AddLiverRoomWidget(QWidget): 536 | """添加直播间 - 独立弹窗""" 537 | roomList = pyqtSignal(dict) 538 | 539 | def __init__(self,application_path): 540 | super(AddLiverRoomWidget, self).__init__() 541 | self.application_path = application_path 542 | self.resize(600, 900) 543 | self.setWindowTitle('添加直播间(房号太多的话尽量分批次添加 避免卡死)') 544 | self.hotLiverDict = {0: [], 1: [], 2: [], 3: [], 4: [], 5: []} 545 | self.followLiverList = [] 546 | layout = QGridLayout(self) 547 | layout.addWidget(QLabel('请输入B站直播间房号 多个房号之间用空格隔开'), 0, 0, 1, 4) 548 | self.roomEditText = '' 549 | self.roomEdit = QLineEdit() 550 | # self.roomEdit.textChanged.connect(self.editChange) # 手感不好 还是取消了 551 | layout.addWidget(self.roomEdit, 1, 0, 1, 5) 552 | confirm = QPushButton('完成') 553 | confirm.setFixedHeight(28) 554 | confirm.clicked.connect(self.sendSelectedRoom) 555 | confirm.setStyleSheet('background-color:#3daee9') 556 | layout.addWidget(confirm, 0, 4, 1, 1) 557 | 558 | tab = QTabWidget() 559 | layout.addWidget(tab, 2, 0, 5, 5) 560 | 561 | hotLiverPage = QWidget() 562 | hotLiverLayout = QGridLayout(hotLiverPage) 563 | hotLiverLayout.setContentsMargins(1, 1, 1, 1) 564 | 565 | self.virtual = PushButton('虚拟主播', True) 566 | self.virtual.clicked.connect(lambda: self.switchHotLiver(0)) 567 | hotLiverLayout.addWidget(self.virtual, 0, 0, 1, 1) 568 | self.onlineGame = PushButton('网游') 569 | self.onlineGame.clicked.connect(lambda: self.switchHotLiver(1)) 570 | hotLiverLayout.addWidget(self.onlineGame, 0, 1, 1, 1) 571 | self.mobileGame = PushButton('手游') 572 | self.mobileGame.clicked.connect(lambda: self.switchHotLiver(2)) 573 | hotLiverLayout.addWidget(self.mobileGame, 0, 2, 1, 1) 574 | self.consoleGame = PushButton('单机') 575 | self.consoleGame.clicked.connect(lambda: self.switchHotLiver(3)) 576 | hotLiverLayout.addWidget(self.consoleGame, 0, 3, 1, 1) 577 | self.entertainment = PushButton('娱乐') 578 | self.entertainment.clicked.connect(lambda: self.switchHotLiver(4)) 579 | hotLiverLayout.addWidget(self.entertainment, 0, 4, 1, 1) 580 | # self.broadcasting = PushButton('电台') 581 | # self.broadcasting.clicked.connect(lambda: self.switchHotLiver(5)) 582 | # hotLiverLayout.addWidget(self.broadcasting, 0, 5, 1, 1) 583 | self.buttonList = [self.virtual, self.onlineGame, self.mobileGame, self.consoleGame, self.entertainment] 584 | self.currentPage = 0 585 | 586 | self.progressBar = QProgressBar(self) 587 | self.progressBar.setGeometry(0, 0, self.width(), 20) 588 | self.progressBar.setRange(0,0) 589 | 590 | self.hotLiverTable = HotLiverTable() 591 | self.hotLiverTable.setEditTriggers(QAbstractItemView.NoEditTriggers) 592 | self.hotLiverTable.verticalScrollBar().installEventFilter(self) 593 | self.hotLiverTable.verticalHeader().sectionClicked.connect(self.hotLiverAdd) 594 | self.hotLiverTable.setColumnCount(3) 595 | self.hotLiverTable.setRowCount(100) 596 | self.hotLiverTable.setVerticalHeaderLabels(['添加'] * 100) 597 | self.hotLiverTable.setHorizontalHeaderLabels(['主播名', '直播间标题', '直播间房号']) 598 | self.hotLiverTable.setColumnWidth(0, 130) 599 | self.hotLiverTable.setColumnWidth(1, 240) 600 | self.hotLiverTable.setColumnWidth(2, 130) 601 | self.hotLiverTable.setEnabled(False) #启动时暂时禁用table 602 | hotLiverLayout.addWidget(self.hotLiverTable, 1, 0, 1, 5) 603 | self.getHotLiver = GetHotLiver() 604 | self.getHotLiver.roomInfoSummary.connect(self.collectHotLiverInfo) 605 | 606 | followsPage = QWidget() 607 | followsLayout = QGridLayout(followsPage) 608 | followsLayout.setContentsMargins(0, 0, 0, 0) 609 | followsLayout.addWidget(QLabel(), 0, 2, 1, 1) 610 | followsLayout.addWidget(QLabel('自动添加你关注的up直播间 (只能拉取最近关注的500名)'), 0, 3, 1, 3) 611 | self.uidEdit = QLineEdit() 612 | self.uidEdit.setPlaceholderText('请输入你的uid') 613 | self.uidEdit.setMinimumWidth(120) 614 | self.uidEdit.setMaximumWidth(300) 615 | followsLayout.addWidget(self.uidEdit, 0, 0, 1, 1) 616 | uidCheckButton = QPushButton('查询') 617 | uidCheckButton.setFixedHeight(27) 618 | uidCheckButton.setStyleSheet('background-color:#3daee9') 619 | uidCheckButton.clicked.connect(self.checkFollows) # 查询关注 620 | followsLayout.addWidget(uidCheckButton, 0, 1, 1, 1) 621 | self.followsTable = QTableWidget() 622 | self.followsTable.setEditTriggers(QAbstractItemView.NoEditTriggers) 623 | self.followsTable.verticalScrollBar().installEventFilter(self) 624 | self.followsTable.verticalHeader().sectionClicked.connect(self.followLiverAdd) 625 | self.followsTable.setColumnCount(3) 626 | self.followsTable.setRowCount(500) 627 | self.followsTable.setVerticalHeaderLabels(['添加'] * 500) 628 | self.followsTable.setHorizontalHeaderLabels(['主播名', '直播间标题', '直播间房号']) 629 | self.followsTable.setColumnWidth(0, 130) 630 | self.followsTable.setColumnWidth(1, 240) 631 | self.followsTable.setColumnWidth(2, 130) 632 | followsLayout.addWidget(self.followsTable, 1, 0, 6, 6) 633 | self.getFollows = GetFollows() 634 | self.getFollows.roomInfoSummary.connect(self.collectFollowLiverInfo) 635 | 636 | hacoPage = QWidget() # 添加内置的vtb列表 637 | hacoLayout = QGridLayout(hacoPage) 638 | hacoLayout.setContentsMargins(1, 1, 1, 1) 639 | self.refreshButton = PushButton('更新名单') 640 | self.refreshButton.clicked.connect(self.refreshHacoList) 641 | hacoLayout.addWidget(self.refreshButton, 0, 0, 1, 1) 642 | self.vtbSearchButton = PushButton('查询VUP') 643 | self.vtbSearchButton.clicked.connect(self.vtbSearch) 644 | hacoLayout.addWidget(self.vtbSearchButton, 0, 1, 1, 1) 645 | self.hacoTable = QTableWidget() 646 | self.hacoTable.setEditTriggers(QAbstractItemView.NoEditTriggers) 647 | self.hacoTable.verticalScrollBar().installEventFilter(self) 648 | self.hacoTable.verticalHeader().sectionClicked.connect(self.hacoAdd) 649 | self.hacoTable.setColumnCount(3) 650 | try: 651 | self.vtbList = [] 652 | vtbs = codecs.open(os.path.join(self.application_path,'utils/vtb.csv'), 'r', encoding='utf-8') 653 | for line in vtbs: 654 | line = line.strip() 655 | if line: 656 | self.vtbList.append(line.split(',')) 657 | else: 658 | self.vtbList.append(['', '', '']) 659 | vtbs.close() 660 | self.hacoTable.setRowCount(len(self.vtbList)) 661 | self.hacoTable.setVerticalHeaderLabels(['添加'] * len(self.vtbList)) 662 | for y, line in enumerate(self.vtbList): 663 | for x in range(3): 664 | self.hacoTable.setItem(y, x, QTableWidgetItem(line[x])) 665 | except: 666 | logging.exception('vtb.csv 解析失败') 667 | 668 | self.hacoTable.setHorizontalHeaderLabels(['主播名', '直播间房号', '所属']) 669 | self.hacoTable.setColumnWidth(0, 160) 670 | self.hacoTable.setColumnWidth(1, 160) 671 | self.hacoTable.setColumnWidth(2, 160) 672 | hacoLayout.addWidget(self.hacoTable, 1, 0, 10, 5) 673 | self.downloadVTBList = DownloadVTBList() 674 | self.downloadVTBList.vtbList.connect(self.collectVTBList) 675 | # self.downloadVTBList.start() 676 | 677 | tab.addTab(hotLiverPage, '正在直播') 678 | tab.addTab(hacoPage, '个人势/箱') 679 | tab.addTab(followsPage, '关注添加') 680 | 681 | def editChange(self): # 提取输入文本中的数字 682 | if len(self.roomEdit.text()) > len(self.roomEditText): 683 | roomEditText = '' 684 | roomIDList = self.roomEdit.text().split(' ') 685 | for roomID in roomIDList: 686 | strList = map(lambda x: x if x.isdigit() else '', roomID) 687 | roomID = '' 688 | digitToken = False 689 | for s in strList: 690 | if s: 691 | roomID += s 692 | digitToken = True 693 | elif digitToken: 694 | roomID += ' ' 695 | if roomID not in roomEditText: 696 | roomEditText += roomID 697 | roomID = '' 698 | digitToken = False 699 | if roomID: 700 | roomID += ' ' 701 | if roomID not in roomEditText: 702 | roomEditText += roomID 703 | self.roomEdit.setText(roomEditText) 704 | self.roomEditText = roomEditText 705 | 706 | def closeEvent(self, event): 707 | if self.getHotLiver.isRunning(): 708 | self.getHotLiver.terminate() 709 | 710 | def collectHotLiverInfo(self, info): 711 | self.hotLiverDict = {} 712 | self.progressBar.hide() 713 | self.hotLiverTable.setEnabled(True) 714 | for page, hotLiverList in enumerate(info): 715 | self.hotLiverDict[page] = hotLiverList 716 | for y, line in enumerate(hotLiverList): 717 | for x, txt in enumerate(line): 718 | if page == self.currentPage: 719 | try: 720 | self.hotLiverTable.setItem(y, x, QTableWidgetItem(txt)) 721 | except: 722 | logging.exception('热门直播表插入失败') 723 | 724 | def switchHotLiver(self, index): 725 | if not self.buttonList[index].pushToken: 726 | self.currentPage = index 727 | for cnt, button in enumerate(self.buttonList): 728 | if cnt == index: # 点击的按钮 729 | button.pushToken = True 730 | button.setStyleSheet('background-color:#3daee9;border-width:1px') 731 | else: 732 | button.pushToken = False 733 | button.setStyleSheet('background-color:#31363b;border-width:1px') 734 | self.hotLiverTable.clear() 735 | self.hotLiverTable.setColumnCount(3) 736 | self.hotLiverTable.setRowCount(100) 737 | self.hotLiverTable.setVerticalHeaderLabels(['添加'] * 100) 738 | self.hotLiverTable.setHorizontalHeaderLabels(['主播名', '直播间标题', '直播间房号']) 739 | self.hotLiverTable.setColumnWidth(0, 130) 740 | self.hotLiverTable.setColumnWidth(1, 240) 741 | self.hotLiverTable.setColumnWidth(2, 130) 742 | hotLiverList = self.hotLiverDict[index] 743 | for y, line in enumerate(hotLiverList): 744 | for x, txt in enumerate(line): 745 | try: 746 | self.hotLiverTable.setItem(y, x, QTableWidgetItem(txt)) 747 | except: 748 | logging.exception('热门直播表更换失败') 749 | 750 | def refreshHacoList(self): 751 | self.refreshButton.clicked.disconnect(self.refreshHacoList) 752 | self.refreshButton.setText('更新中...') 753 | self.downloadVTBList.start() 754 | 755 | def vtbSearch(self): 756 | QDesktopServices.openUrl(QUrl(r'https://vtbs.moe/detail')) 757 | 758 | def collectVTBList(self, vtbList): 759 | try: 760 | vtbs = codecs.open(os.path.join(self.application_path, 'utils/vtb.csv'), 'w', encoding='utf-8') 761 | for line in vtbList: 762 | vtbs.write(line) 763 | vtbs.close() 764 | self.vtbList = [] 765 | for line in vtbList: 766 | self.vtbList.append(line.split(',')) 767 | self.hacoTable.clear() 768 | self.hacoTable.setRowCount(len(self.vtbList)) 769 | self.hacoTable.setVerticalHeaderLabels(['添加'] * len(self.vtbList)) 770 | self.hacoTable.setHorizontalHeaderLabels(['主播名', '直播间房号', '所属']) 771 | for y, line in enumerate(self.vtbList): 772 | for x in range(3): 773 | self.hacoTable.setItem(y, x, QTableWidgetItem(line[x])) 774 | QMessageBox.information(self, '更新VUP名单', '更新完成', QMessageBox.Ok) 775 | except: 776 | logging.exception('vtb.csv 写入失败') 777 | QMessageBox.information(self, '更新VUP名单', '更新失败 请检查网络', QMessageBox.Ok) 778 | self.refreshButton.setText('更新名单') 779 | self.refreshButton.clicked.connect(self.refreshHacoList) 780 | 781 | def sendSelectedRoom(self): 782 | self.closeEvent(None) 783 | tmpList = self.roomEdit.text().strip().replace('\t', ' ').split(' ') 784 | roomList = {} 785 | for i in tmpList: 786 | if i.isnumeric(): 787 | roomList[i] = False # 全部统一为字符串格式的roomid 788 | self.roomList.emit(roomList) 789 | self.roomEdit.clear() 790 | self.hide() 791 | 792 | def hotLiverAdd(self, row): 793 | try: 794 | hotLiverList = self.hotLiverDict[self.currentPage] 795 | roomID = hotLiverList[row][2] 796 | addedRoomID = self.roomEdit.text() 797 | if roomID not in addedRoomID: 798 | addedRoomID += ' %s' % roomID 799 | self.roomEdit.setText(addedRoomID) 800 | except: 801 | logging.exception('热门主播添加失败') 802 | 803 | def hacoAdd(self, row): 804 | try: 805 | roomID = self.vtbList[row][1] 806 | if roomID: 807 | addedRoomID = self.roomEdit.text() 808 | if roomID not in addedRoomID: 809 | addedRoomID += ' %s' % roomID 810 | self.roomEdit.setText(addedRoomID) 811 | except: 812 | logging.exception('hacoAdd 失败') 813 | 814 | def checkFollows(self): 815 | if self.uidEdit.text().isdigit(): 816 | self.getFollows.setUID(self.uidEdit.text()) 817 | self.getFollows.start() 818 | 819 | def collectFollowLiverInfo(self, info): 820 | self.followLiverList = [] 821 | for y, line in enumerate(info): 822 | self.followLiverList.append(line[2]) 823 | for x, txt in enumerate(line): 824 | try: 825 | self.followsTable.setItem(y, x, QTableWidgetItem(txt)) 826 | except: 827 | logging.exception('关注列表添加失败') 828 | 829 | def followLiverAdd(self, row): 830 | try: 831 | roomID = self.followLiverList[row] 832 | addedRoomID = self.roomEdit.text() 833 | if roomID not in addedRoomID: 834 | addedRoomID += ' %s' % roomID 835 | self.roomEdit.setText(addedRoomID) 836 | except: 837 | logging.exception('关注列表添加失败') 838 | 839 | 840 | class CollectLiverInfo(QThread): 841 | """批量获取直播间信息 842 | + 直播状态 'live_status' 843 | + 标题 'title' 844 | + 封面 'cover' 845 | + 关键帧 'keyframe' 846 | + 头像 'face' 847 | 848 | TODO: 849 | + bilibili_api.live.get_room_info(room_id) 850 | """ 851 | liverInfo = pyqtSignal(list) 852 | 853 | def __init__(self, roomIDList): 854 | super(CollectLiverInfo, self).__init__() 855 | self.roomIDList = roomIDList 856 | 857 | def setRoomIDList(self, roomIDList): 858 | self.roomIDList = roomIDList 859 | 860 | def run(self): 861 | logging.debug("Collecting Liver Info...") 862 | while 1: 863 | try: 864 | liverInfo = [] 865 | data = json.dumps({'ids': self.roomIDList}) # 根据直播间房号批量获取直播间信息 866 | r = requests.post(r'https://api.live.bilibili.com/room/v2/Room/get_by_ids', data=data) 867 | r.encoding = 'utf8' 868 | data = json.loads(r.text)['data'] 869 | uidList = [] 870 | for roomID in data: 871 | uidList.append(data[roomID]['uid']) 872 | data = json.dumps({'uids': uidList}) 873 | r = requests.post(r'https://api.live.bilibili.com/room/v1/Room/get_status_info_by_uids', data=data) 874 | r.encoding = 'utf8' 875 | data = json.loads(r.text)['data'] 876 | if data: 877 | for roomID in self.roomIDList: 878 | exist = False 879 | for uid, info in data.items(): 880 | if roomID == info['room_id']: 881 | title = info['title'] 882 | uname = info['uname'] 883 | face = info['face'] 884 | liveStatus = info['live_status'] 885 | keyFrame = info['keyframe'] 886 | exist = True 887 | liverInfo.append([uid, str(roomID), uname, face, liveStatus, keyFrame, title]) 888 | break 889 | try: 890 | if not exist: 891 | r = requests.get(r'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=%s' % roomID) 892 | r.encoding = 'utf8' 893 | banData = json.loads(r.text)['data'] 894 | if banData: 895 | try: 896 | uname = banData['anchor_info']['base_info']['uname'] 897 | except: 898 | uname = '' 899 | else: 900 | uname = '' 901 | liverInfo.append([None, str(roomID), uname]) 902 | except Exception as e: 903 | logging.error(str(e)) 904 | if liverInfo: 905 | self.liverInfo.emit(liverInfo) 906 | time.sleep(60) # 冷却时间 907 | except Exception as e: 908 | logging.error(str(e)) 909 | 910 | 911 | class LiverPanel(QWidget): 912 | """关注的直播间""" 913 | addToWindow = pyqtSignal(list) 914 | dumpConfig = pyqtSignal() 915 | refreshIDList = pyqtSignal(list) 916 | startLiveList = pyqtSignal(list) 917 | 918 | def __init__(self, roomIDDict, app_path): 919 | super(LiverPanel, self).__init__() 920 | self.application_path = app_path 921 | self.refreshCount = 0 922 | self.oldLiveStatus = {} 923 | self.addLiverRoomWidget = AddLiverRoomWidget(self.application_path) 924 | self.addLiverRoomWidget.roomList.connect(self.addLiverRoomList) 925 | self.addLiverRoomWidget.hotLiverTable.addToWindow.connect(self.addCoverToPlayer) 926 | self.multiple = 1 927 | self.layout = QGridLayout(self) 928 | self.layout.setSpacing(9) 929 | self.layout.setContentsMargins(7, 7, 7, 7) 930 | self.coverList = [] 931 | for roomID, topToken in roomIDDict.items(): 932 | self.coverList.append(CoverLabel(roomID, topToken)) 933 | self.coverList[-1].addToWindow.connect(self.addCoverToPlayer) # 添加至窗口播放信号 934 | self.coverList[-1].deleteCover.connect(self.deleteCover) 935 | self.coverList[-1].changeTopToken.connect(self.changeTop) 936 | for cover in self.coverList: # 先添加置顶卡片 937 | if cover.topToken: 938 | self.layout.addWidget(cover) 939 | for cover in self.coverList: # 再添加普通卡片 940 | if not cover.topToken: 941 | self.layout.addWidget(cover) 942 | self.roomIDDict = roomIDDict 943 | self.collectLiverInfo = CollectLiverInfo(list(map(int, self.roomIDDict.keys()))) # 转成整型 944 | self.collectLiverInfo.liverInfo.connect(self.refreshRoomPanel) 945 | self.collectLiverInfo.start() 946 | 947 | def openLiverRoomPanel(self): 948 | self.addLiverRoomWidget.getHotLiver.start() 949 | self.addLiverRoomWidget.hide() 950 | self.addLiverRoomWidget.show() 951 | 952 | def addLiverRoomList(self, roomDict): 953 | logging.debug("接收到新的主播列表") 954 | newID = [] 955 | for roomID, topToken in roomDict.items(): # 如果id不在老列表里面 则添加 956 | if len(roomID) <= 4: # 查询短号 957 | try: 958 | r = requests.get('https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=%s' % roomID) 959 | data = json.loads(r.text)['data'] 960 | roomID = str(data['room_info']['room_id']) 961 | # print(roomID) 962 | except: 963 | logging.exception('房间号查询失败') 964 | if roomID not in self.roomIDDict: 965 | newID.append(roomID) 966 | else: 967 | for cover in self.coverList: 968 | if cover.roomID == roomID: 969 | cover.topToken = topToken 970 | break 971 | for index, roomID in enumerate(newID): # 添加id并创建新的预览图卡 972 | if roomID in roomDict: 973 | self.coverList.append(CoverLabel(roomID, roomDict[roomID])) 974 | else: 975 | self.coverList.append(CoverLabel(roomID, False)) # 添加短号的原长号 976 | self.coverList[-1].addToWindow.connect(self.addCoverToPlayer) # 添加至播放窗口 977 | self.coverList[-1].deleteCover.connect(self.deleteCover) 978 | self.coverList[-1].changeTopToken.connect(self.changeTop) 979 | self.roomIDDict[str(roomID)] = False # 添加普通卡片 字符串类型 980 | self.collectLiverInfo.setRoomIDList(list(map(int, self.roomIDDict.keys()))) # 更新需要刷新的房间列表 981 | self.collectLiverInfo.terminate() 982 | self.collectLiverInfo.wait() 983 | self.collectLiverInfo.start() 984 | self.dumpConfig.emit() # 发送保存config信号 985 | self.refreshPanel() 986 | 987 | def refreshRoomPanel(self, liverInfo): # 异步刷新图卡 988 | self.refreshCount += 1 # 刷新计数+1 989 | roomIDToRefresh = [] 990 | roomIDStartLive = [] 991 | firstRefresh = False 992 | for index, info in enumerate(liverInfo): 993 | if info[0]: # uid有效 994 | for cover in self.coverList: 995 | if cover.roomID == info[1]: # 字符串房号 996 | if cover.recordState == 2 and cover.liveState == 0 and info[4] == 1: # 满足等待开播录制的3个条件 997 | cover.recordThread.setSavePath(cover.savePath) # 启动录制线程 998 | cover.recordThread.start() 999 | cover.recordThread.checkTimer.start(3000) 1000 | cover.recordState = 1 # 改为录制状态 1001 | elif cover.recordState == 1 and info[4] != 1: # 满足停止录制的2个条件 1002 | cover.recordState = 0 # 取消录制 1003 | cover.recordThread.recordToken = False # 设置录像线程标志位让它自行退出结束 1004 | cover.updateLabel(info) # 更新数据 1005 | if info[1] not in self.oldLiveStatus: # 软件启动后第一次更新添加 1006 | self.oldLiveStatus[info[1]] = info[4] # 房号: 直播状态 1007 | firstRefresh = True # 第一次刷新 1008 | elif self.oldLiveStatus[info[1]] != info[4]: # 状态发生变化 1009 | if info[4] == 1: 1010 | roomIDStartLive.append(info[2]) # 添加开播主播名字 1011 | roomIDToRefresh.append(info[1]) # 发送给主界面要刷新的房间号 1012 | self.oldLiveStatus[info[1]] = info[4] # 更新旧的直播状态列表 1013 | else: # 错误的房号 1014 | for cover in self.coverList: 1015 | if cover.roomID == info[1]: 1016 | cover.updateLabel(info) 1017 | if roomIDStartLive: 1018 | self.startLiveList.emit(roomIDStartLive) # 发送开播列表 1019 | # if roomIDToRefresh: 1020 | # self.refreshIDList.emit(roomIDToRefresh) 1021 | # self.refreshPanel() # 修改刷新策略 只有当有主播直播状态发生变化后才会刷新 降低闪退风险 1022 | # elif firstRefresh: 1023 | # self.refreshPanel() 1024 | # elif not self.refreshCount % 3: # 每20s x 3 = 1分钟强制刷新一次 1025 | # self.refreshPanel() 1026 | self.refreshPanel() 1027 | 1028 | def addCoverToPlayer(self, info): 1029 | self.addToWindow.emit(info) 1030 | 1031 | def deleteCover(self, roomID): 1032 | del self.roomIDDict[roomID] # 删除roomID 1033 | self.collectLiverInfo.setRoomIDList(list(map(int, self.roomIDDict.keys()))) # 更新需要刷新的房间列表 1034 | self.refreshPanel() 1035 | self.dumpConfig.emit() # 发送保存config信号 1036 | 1037 | def deleteAll(self): # 清空卡片槽 1038 | self.roomIDDict.clear() 1039 | self.roomIDList = [] 1040 | for cover in self.coverList: 1041 | cover.roomID = '0' 1042 | self.refreshPanel() 1043 | self.dumpConfig.emit() 1044 | 1045 | def changeTop(self, info): 1046 | self.roomIDDict[int(info[0])] = info[1] # 房号 置顶token 1047 | self.refreshPanel() 1048 | self.dumpConfig.emit() # 发送保存config信号 1049 | 1050 | def updatePlayingStatus(self, playerList): 1051 | for cover in self.coverList: 1052 | if cover.roomID in playerList: 1053 | cover.isPlaying = True 1054 | cover.setStyleSheet('#cover{border-width:3px;border-style:solid;border-color:red;background-color:#5a636d}') 1055 | else: 1056 | cover.isPlaying = False 1057 | if cover.topToken: 1058 | cover.setStyleSheet('#cover{border-width:3px;border-style:solid;border-color:#dfa616;background-color:#5a636d}') 1059 | else: 1060 | cover.setStyleSheet('#cover{border-width:0px;background-color:#5a636d}') 1061 | 1062 | def refreshPanel(self): 1063 | tmpList = [] 1064 | for topToken in [True, False]: 1065 | for liveState in [1, 0, -1]: # 按顺序添加正在直播的 没在直播的 还有错误的卡片 1066 | for cover in self.coverList: 1067 | if cover.liveState == liveState and cover.topToken == topToken and cover.roomID != '0': # 符合条件的卡片 1068 | tmpList.append(cover) 1069 | else: 1070 | cover.hide() 1071 | # self.layout.addWidget(cover) 1072 | # for cover in self.coverList: 1073 | # cover.hide() 1074 | # if cover.liveState in [1, 0, -1] and cover.roomID != '0': 1075 | # cover.show() 1076 | for cnt, cover in enumerate(tmpList): 1077 | self.layout.addWidget(cover, cnt // self.multiple, cnt % self.multiple) 1078 | cover.show() 1079 | self.adjustSize() 1080 | -------------------------------------------------------------------------------- /QR/Alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhimingshenjun/DD_Monitor/0e7fe77a370c69a4dec5aefe29ea99f21760c2b1/QR/Alipay.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DD监控室 2 | 3 | ## 运行指南 4 | 5 | 确保安装VLC, `HELP.html`内有更多解释。 6 | 7 | ## 开发指南 8 | 9 | 推荐在venv/anaconda环境下开发 10 | 11 | ### 所需包 12 | - PyQt5 13 | 14 | - requests 15 | 16 | - aiowebsocket 17 | 18 | - python-vlc 19 | 20 | - pyinstaller 21 | 22 | - dnspython 23 | 24 | 25 | 26 | pip安装 27 | 28 | ```bash 29 | pip install -r requirements.txt 30 | ``` 31 | 32 | 33 | 34 | ### 打包 35 | 36 | 在 `scripts` 文件夹下有各平台的打包脚本,需要在仓库根目录运行。 37 | 38 | ## TODO 39 | 40 | ### 全平台 41 | - [X] 加载热播主播时显示加载状态(在Macos上有明显卡顿) 42 | 43 | ### Windows平台 44 | - [ ] ? 45 | 46 | ### MacOS平台 47 | - [X] 保证程序打包后附带文件可以被访问 48 | - [ ] 弹幕窗口在启动后不显示 49 | - [X] 添加热播主播后Thread卡死 50 | - [ ] ~~添加主播到播放器后不会继承窗口大小,需要重整layout来激活~~ (VLC bug) 51 | 52 | ### Linux平台 53 | - [ ] 弹幕窗口在启动后不显示 -------------------------------------------------------------------------------- /ReportException.py: -------------------------------------------------------------------------------- 1 | """ 2 | 异常捕获器 3 | """ 4 | import os, sys, time, datetime, subprocess, logging, traceback, platform 5 | from PyQt5.Qt import * 6 | 7 | 8 | def uncaughtExceptionHandler(exctype, value, tb): 9 | logging.error("\n************!!!UNCAUGHT EXCEPTION!!!*********************\n" + 10 | ("Type: %s" % exctype) + '\n' + 11 | ("Value: %s" % value) + '\n' + 12 | ("Traceback:" + '\n') + 13 | " ".join(traceback.format_tb(tb)) + 14 | "************************************************************\n") 15 | showFaultDialog(err_type=exctype, err_value=value, tb=tb) 16 | 17 | 18 | def unraisableExceptionHandler(exc_type,exc_value,exc_traceback,err_msg,object): 19 | logging.error("\n************!!!UNHANDLEABLE EXCEPTION!!!******************\n" + 20 | ("Type: %s" % exc_type) + '\n' + 21 | ("Value: %s" % exc_value) + '\n' + 22 | ("Message: %s " % err_msg) + '\n' + 23 | ("Traceback:" + '\n') + 24 | " ".join(traceback.format_tb(exc_traceback)) + '\n' + 25 | ("On Object: %s" + object) + '\n' + 26 | "************************************************************\n") 27 | showFaultDialog(err_type=exc_type, err_value=exc_value, tb=exc_traceback) 28 | 29 | 30 | def thraedingExceptionHandler(exc_type,exc_value,exc_traceback,thread): 31 | logging.error("\n************!!!UNCAUGHT THREADING EXCEPTION!!!***********\n" + 32 | ("Type: %s" % exc_type) + '\n' + 33 | ("Value: %s" % exc_value) + '\n' + 34 | ("Traceback on thread %s: " % thread + '\n') + 35 | " ".join(traceback.format_tb(exc_traceback)) + 36 | "************************************************************\n") 37 | showFaultDialog(err_type=exc_type, err_value=exc_value, tb=exc_traceback) 38 | 39 | 40 | def loggingSystemInfo(): 41 | systemCmd = "" 42 | gpuCmd = "" 43 | if platform.system() == 'Windows': 44 | systemCmd = r"\u C:\Windows\System32\systeminfo.exe" 45 | wmi_exe = r"C:\Windows\System32\wbem\WMIC.exe" 46 | # cmd 下运行 "wmic PATH win32_VideoController GET /?" 查看可查询的参数列表 47 | gpu_property_list = "AdapterCompatibility, Caption, DeviceID, DriverDate, DriverVersion, VideoModeDescription" 48 | gpuCmd = f"{wmi_exe} PATH win32_VideoController GET {gpu_property_list} /FORMAT:list" 49 | elif platform.system() == 'Darwin': 50 | systemCmd = "/usr/sbin/system_profiler SPHardwareDataType" 51 | gpuCmd = "/usr/sbin/system_profiler SPDisplaysDataType" 52 | elif platform.system() == 'Linux': 53 | systemCmd = "/usr/bin/lscpu" 54 | gpuCmd = "/usr/bin/lspci" 55 | 56 | systemInfoProcess = subprocess.Popen(systemCmd, shell=True, stdout=subprocess.PIPE,universal_newlines=True) 57 | systemInfoProcessReturn = systemInfoProcess.stdout.read() 58 | gpuInfoProcess = subprocess.Popen(gpuCmd, shell=True, stdout=subprocess.PIPE, universal_newlines=True) 59 | gpuInfoProcessReturn = gpuInfoProcess.stdout.read() 60 | 61 | if platform.system() == 'Windows': 62 | gpuInfoProcessReturn = gpuInfoProcessReturn.strip() 63 | gpuInfoProcessReturn = gpuInfoProcessReturn.replace("\n\n", "\n") 64 | 65 | logging.info(f"系统信息: \n{systemInfoProcessReturn}") 66 | logging.info(f"GPU信息: \n{gpuInfoProcessReturn}") 67 | 68 | 69 | def showFaultDialog(err_type, err_value, tb): 70 | return None # 跳过报错 没精力改了 71 | msg = QMessageBox() 72 | msg.setIcon(QMessageBox.Critical) 73 | msg.setText("不好,出现了一个问题: %s" % err_type) 74 | msg.setInformativeText("运行中出现了%s故障,日志已保存在logs文件夹中的log-%s.txt文件内。" % (err_value, datetime.datetime.today().strftime('%Y-%m-%d'))) 75 | msg.setWindowTitle("DD监控室出现了问题") 76 | msg.setDetailedText("Traceback:\n%s" % (" ".join(traceback.format_tb(tb)))) 77 | msg.setStandardButtons(QMessageBox.Ok) 78 | msg.exec_() 79 | -------------------------------------------------------------------------------- /VideoWidget.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from PyQt5.QtWidgets import * # QAction,QFileDialog 4 | from PyQt5.QtGui import * # QIcon,QPixmap 5 | from PyQt5.QtCore import * # QSize 6 | from PyQt5.QtMultimedia import * 7 | from PyQt5.QtMultimediaWidgets import QGraphicsVideoItem 8 | from remote import remoteThread 9 | 10 | 11 | class Bar(QLabel): 12 | moveSignal = pyqtSignal(QPoint) 13 | 14 | def __init__(self, text): 15 | super(Bar, self).__init__() 16 | self.setText(text) 17 | self.setFixedHeight(25) 18 | 19 | def mousePressEvent(self, event): 20 | self.startPos = event.pos() 21 | 22 | def mouseMoveEvent(self, event): 23 | self.moveSignal.emit(event.pos() - self.startPos) 24 | 25 | 26 | class ToolButton(QToolButton): 27 | def __init__(self, icon): 28 | super(ToolButton, self).__init__() 29 | self.setStyleSheet('border-color:#CCCCCC') 30 | self.setFixedSize(25, 25) 31 | self.setIcon(icon) 32 | 33 | 34 | class TextOpation(QWidget): 35 | def __init__(self, setting=[20, 2, 6, 0, '【 [ {']): 36 | super(TextOpation, self).__init__() 37 | self.resize(300, 300) 38 | self.setWindowTitle('弹幕窗设置') 39 | self.setWindowFlag(Qt.WindowStaysOnTopHint) 40 | layout = QGridLayout(self) 41 | layout.addWidget(QLabel('窗体透明度'), 0, 0, 1, 1) 42 | self.opacitySlider = Slider() 43 | self.opacitySlider.setValue(setting[0]) 44 | layout.addWidget(self.opacitySlider, 0, 1, 1, 1) 45 | layout.addWidget(QLabel('窗体横向占比'), 1, 0, 1, 1) 46 | self.horizontalCombobox = QComboBox() 47 | self.horizontalCombobox.addItems( 48 | ['10%', '15%', '20%', '25%', '30%', '35%', '40%', '45%', '50%']) 49 | self.horizontalCombobox.setCurrentIndex(setting[1]) 50 | layout.addWidget(self.horizontalCombobox, 1, 1, 1, 1) 51 | layout.addWidget(QLabel('窗体纵向占比'), 2, 0, 1, 1) 52 | self.verticalCombobox = QComboBox() 53 | self.verticalCombobox.addItems( 54 | ['50%', '55%', '60%', '65%', '70%', '75%', '80%', '85%', '90%', '95%', '100%']) 55 | self.verticalCombobox.setCurrentIndex(setting[2]) 56 | layout.addWidget(self.verticalCombobox, 2, 1, 1, 1) 57 | layout.addWidget(QLabel('单独同传窗口'), 3, 0, 1, 1) 58 | self.translateCombobox = QComboBox() 59 | self.translateCombobox.addItems(['开启', '关闭']) 60 | self.translateCombobox.setCurrentIndex(setting[3]) 61 | layout.addWidget(self.translateCombobox, 3, 1, 1, 1) 62 | layout.addWidget(QLabel('同传过滤字符 (空格隔开)'), 4, 0, 1, 1) 63 | self.translateFitler = QLineEdit('') 64 | self.translateFitler.setText(setting[4]) 65 | self.translateFitler.setFixedWidth(100) 66 | layout.addWidget(self.translateFitler, 4, 1, 1, 1) 67 | 68 | 69 | class TextBrowser(QWidget): 70 | closeSignal = pyqtSignal() 71 | 72 | def __init__(self, parent, id): 73 | super(TextBrowser, self).__init__(parent) 74 | self.optionWidget = TextOpation() 75 | 76 | layout = QGridLayout(self) 77 | layout.setContentsMargins(0, 0, 0, 0) 78 | layout.setSpacing(0) 79 | self.bar = Bar(' 窗口%s 弹幕' % (id + 1)) 80 | self.bar.setStyleSheet('background:#AAAAAAAA') 81 | self.bar.moveSignal.connect(self.moveWindow) 82 | layout.addWidget(self.bar, 0, 0, 1, 10) 83 | 84 | self.optionButton = ToolButton( 85 | self.style().standardIcon(QStyle.SP_FileDialogDetailedView)) 86 | self.optionButton.clicked.connect(self.optionWidget.show) # 弹出设置菜单 87 | layout.addWidget(self.optionButton, 0, 8, 1, 1) 88 | 89 | self.closeButton = ToolButton( 90 | self.style().standardIcon(QStyle.SP_TitleBarCloseButton)) 91 | self.closeButton.clicked.connect(self.userClose) 92 | layout.addWidget(self.closeButton, 0, 9, 1, 1) 93 | 94 | self.textBrowser = QTextBrowser() 95 | self.textBrowser.setFont(QFont('Microsoft JhengHei', 16, QFont.Bold)) 96 | self.textBrowser.setStyleSheet('border-width:1') 97 | layout.addWidget(self.textBrowser, 1, 0, 1, 10) 98 | 99 | self.transBrowser = QTextBrowser() 100 | self.transBrowser.setFont(QFont('Microsoft JhengHei', 16, QFont.Bold)) 101 | self.transBrowser.setStyleSheet('border-width:1') 102 | # self.transBrowser.setFixedHeight(self.height() / 3) 103 | layout.addWidget(self.transBrowser, 2, 0, 1, 10) 104 | 105 | def userClose(self): 106 | self.hide() 107 | self.closeSignal.emit() 108 | 109 | def moveWindow(self, moveDelta): 110 | newPos = self.pos() + moveDelta 111 | x, y = newPos.x(), newPos.y() 112 | rightBorder = self.parent().width() - self.width() 113 | bottomBoder = self.parent().height() - self.height() 114 | if x < 0: 115 | x = 0 116 | elif x > rightBorder: 117 | x = rightBorder 118 | if y < 0: 119 | y = 0 120 | elif y > bottomBoder: 121 | y = bottomBoder 122 | self.move(x, y) 123 | 124 | 125 | class PushButton(QPushButton): 126 | def __init__(self, icon='', text=''): 127 | super(PushButton, self).__init__() 128 | self.setFixedSize(30, 30) 129 | self.setStyleSheet('background-color:#00000000') 130 | if icon: 131 | self.setIcon(icon) 132 | elif text: 133 | self.setText(text) 134 | 135 | 136 | class Slider(QSlider): 137 | value = pyqtSignal(int) 138 | 139 | def __init__(self, value=100): 140 | super(Slider, self).__init__() 141 | self.setOrientation(Qt.Horizontal) 142 | self.setFixedWidth(100) 143 | self.setValue(value) 144 | 145 | def mousePressEvent(self, event): 146 | self.updateValue(event.pos()) 147 | 148 | def mouseMoveEvent(self, event): 149 | self.updateValue(event.pos()) 150 | 151 | def wheelEvent(self, event): # 把进度条的滚轮事件去了 用啥子滚轮 152 | pass 153 | 154 | def updateValue(self, QPoint): 155 | value = QPoint.x() 156 | if value > 100: 157 | value = 100 158 | elif value < 0: 159 | value = 0 160 | self.setValue(value) 161 | self.value.emit(value) 162 | 163 | 164 | class GraphicsView(QGraphicsView): 165 | rightClicked = pyqtSignal(QEvent) 166 | 167 | def mouseReleaseEvent(self, event): 168 | if event.button() == Qt.RightButton: 169 | self.rightClicked.emit(event) 170 | 171 | 172 | class GraphicsVideoItem(QGraphicsVideoItem): 173 | dropFile = pyqtSignal(str) # 重写接收drop信号 174 | 175 | def __init__(self, parent=None): 176 | super().__init__(parent) 177 | self.setAcceptDrops(True) 178 | 179 | def dropEvent(self, QEvent): 180 | if QEvent.mimeData().hasText: 181 | self.dropFile.emit(QEvent.mimeData().text()) 182 | 183 | 184 | class GetMediaURL(QThread): 185 | url = pyqtSignal(QMediaContent) 186 | 187 | def __init__(self): 188 | super(GetMediaURL, self).__init__() 189 | self.roomID = 0 190 | self.quality = 250 191 | 192 | def setConfig(self, roomID, quality): 193 | self.roomID = roomID 194 | self.quality = quality 195 | 196 | def getStreamUrl(self): 197 | url = "https://api.live.bilibili.com/xlive/app-room/v2/index/getRoomPlayInfo" 198 | onlyAudio = self.quality < 0 199 | params = { 200 | "appkey": "iVGUTjsxvpLeuDCf", 201 | "build": 6215200, 202 | "c_locale": "zh_CN", 203 | "channel": "bili", 204 | "codec": 0, 205 | "device": "android", 206 | "device_name": "VTR-AL00", 207 | "dolby": 1, 208 | "format": "0,2", 209 | "free_type": 0, 210 | "http": 1, 211 | "mask": 0, 212 | "mobi_app": "android", 213 | "network": "wifi", 214 | "no_playurl": 0, 215 | "only_audio": bool(onlyAudio), 216 | "only_video": 0, 217 | "platform": "android", 218 | "play_type": 0, 219 | "protocol": "0,1", 220 | "qn": (onlyAudio and 10000) or (not onlyAudio and self.quality), 221 | "room_id": self.roomID, 222 | "s_locale": "zh_CN", 223 | "statistics": "{\"appId\":1,\"platform\":3,\"version\":\"6.21.5\",\"abtest\":\"\"}", 224 | "ts": int(time.time()) 225 | } 226 | r = requests.get(url, params=params) 227 | j = r.json() 228 | baseUrl = j['data']['playurl_info']['playurl']['stream'][0]['format'][0]['codec'][0]['base_url'] 229 | extra = j['data']['playurl_info']['playurl']['stream'][0]['format'][0]['codec'][0]['url_info'][0]['extra'] 230 | host = j['data']['playurl_info']['playurl']['stream'][0]['format'][0]['codec'][0]['url_info'][0]['host'] 231 | # let base_url = jqXHR.responseJSON.data.playurl_info.playurl.stream[0].format[0].codec[0].base_url 232 | # let extra = jqXHR.responseJSON.data.playurl_info.playurl.stream[0].format[0].codec[0].url_info[0].extra 233 | # let host = jqXHR.responseJSON.data.playurl_info.playurl.stream[0].format[0].codec[0].url_info[0].host 234 | # streamURL = host + base_url + extra 235 | streamUrl = host + baseUrl + extra 236 | return streamUrl 237 | 238 | def run(self): 239 | # api = r'https://api.live.bilibili.com/room/v1/Room/playUrl?cid=%s&platform=web&qn=%s' % ( 240 | # self.roomID, self.quality) 241 | # r = requests.get(api) 242 | try: 243 | # print(json.loads(r.text)['data']['durl'][0]['url']) 244 | # self.url.emit(QMediaContent( 245 | # QUrl(json.loads(r.text)['data']['durl'][0]['url']))) 246 | self.url.emit(QMediaContent( 247 | QUrl(self.getStreamUrl()))) 248 | except Exception as e: 249 | print(str(e)) 250 | 251 | 252 | class VideoWidget(QWidget): 253 | mutedChanged = pyqtSignal(list) 254 | volumeChanged = pyqtSignal(list) 255 | addMedia = pyqtSignal(list) # 发送新增的直播 256 | deleteMedia = pyqtSignal(int) # 删除选中的直播 257 | exchangeMedia = pyqtSignal(list) # 交换播放窗口 258 | setDanmu = pyqtSignal(list) # 发射弹幕关闭信号 259 | setTranslator = pyqtSignal(list) # 发送同传关闭信号 260 | changeQuality = pyqtSignal(list) # 修改画质 261 | popWindow = pyqtSignal(list) # 弹出悬浮窗 262 | 263 | def __init__(self, id, top=False, title='', resize=[], textSetting=[True, 20, 2, 6, 0, '【 [ {']): 264 | super(VideoWidget, self).__init__() 265 | self.id = id 266 | self.roomID = 0 267 | self.pauseToken = False 268 | self.quality = 250 269 | self.leftButtonPress = False 270 | self.rightButtonPress = False 271 | self.fullScreen = False 272 | self.top = top 273 | self.textSetting = textSetting 274 | self.horiPercent = [0.1, 0.15, 0.2, 0.25, 0.3, 275 | 0.35, 0.4, 0.45, 0.5][self.textSetting[2]] 276 | self.vertPercent = [0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 277 | 0.8, 0.85, 0.9, 0.95, 1][self.textSetting[3]] 278 | self.filters = textSetting[5].split(' ') 279 | self.opacity = 100 280 | if top: 281 | self.setWindowFlag(Qt.WindowStaysOnTopHint) 282 | if title: 283 | self.setWindowTitle('%s %s' % (title, id + 1)) 284 | if resize: 285 | self.resize(resize[0], resize[1]) 286 | layout = QGridLayout(self) 287 | layout.setContentsMargins(0, 0, 0, 0) 288 | 289 | self.scene = QGraphicsScene() 290 | self.view = GraphicsView() 291 | self.view.rightClicked.connect(self.rightMouseClicked) 292 | self.view.setScene(self.scene) 293 | self.view.resize(1280, 720) 294 | self.videoItem = GraphicsVideoItem() 295 | self.videoItem.dropFile.connect(self.dropFile) 296 | # self.videoItem.setSize(QSizeF(self.width(), self.height())) 297 | self.scene.addItem(self.videoItem) 298 | layout.addWidget(self.view, 0, 0, 12, 12) 299 | self.player = QMediaPlayer() 300 | self.player.setVideoOutput(self.videoItem) 301 | 302 | # self.videoWidget = QVideoWidget() 303 | # self.videoWidget.setStyleSheet('background-color:black') 304 | # self.player = QMediaPlayer() 305 | # self.player.setVideoOutput(self.videoWidget) 306 | # layout.addWidget(self.videoWidget, 0, 0, 12, 12) 307 | self.topLabel = QLabel() 308 | # self.topLabel.setAlignment(Qt.AlignCenter) 309 | self.topLabel.setObjectName('frame') 310 | self.topLabel.setStyleSheet("background-color:#BB708090") 311 | # self.topLabel.setFixedHeight(32) 312 | self.topLabel.setFont(QFont('微软雅黑', 15, QFont.Bold)) 313 | layout.addWidget(self.topLabel, 0, 0, 1, 12) 314 | self.topLabel.hide() 315 | 316 | self.frame = QWidget() 317 | self.frame.setObjectName('frame') 318 | self.frame.setStyleSheet("background-color:#BB708090") 319 | self.frame.setFixedHeight(32) 320 | frameLayout = QHBoxLayout(self.frame) 321 | frameLayout.setContentsMargins(0, 0, 0, 0) 322 | layout.addWidget(self.frame, 11, 0, 1, 12) 323 | self.frame.hide() 324 | 325 | self.titleLabel = QLabel() 326 | self.titleLabel.setMaximumWidth(150) 327 | self.titleLabel.setStyleSheet('background-color:#00000000') 328 | self.setTitle() 329 | frameLayout.addWidget(self.titleLabel) 330 | self.play = PushButton(self.style().standardIcon(QStyle.SP_MediaPause)) 331 | self.play.clicked.connect(self.mediaPlay) 332 | frameLayout.addWidget(self.play) 333 | self.reload = PushButton( 334 | self.style().standardIcon(QStyle.SP_BrowserReload)) 335 | self.reload.clicked.connect(self.mediaReload) 336 | frameLayout.addWidget(self.reload) 337 | self.volume = PushButton( 338 | self.style().standardIcon(QStyle.SP_MediaVolume)) 339 | self.volume.clicked.connect(self.mediaMute) 340 | frameLayout.addWidget(self.volume) 341 | self.slider = Slider() 342 | self.slider.setStyleSheet('background-color:#00000000') 343 | self.slider.value.connect(self.setVolume) 344 | frameLayout.addWidget(self.slider) 345 | self.danmuButton = PushButton(text='弹') 346 | self.danmuButton.clicked.connect(self.showDanmu) 347 | frameLayout.addWidget(self.danmuButton) 348 | self.stop = PushButton(self.style().standardIcon( 349 | QStyle.SP_DialogCancelButton)) 350 | self.stop.clicked.connect(self.mediaStop) 351 | frameLayout.addWidget(self.stop) 352 | 353 | self.getMediaURL = GetMediaURL() 354 | self.getMediaURL.url.connect(self.setMedia) 355 | 356 | self.textBrowser = TextBrowser(self, self.id) 357 | self.setDanmuOpacity(self.textSetting[1]) # 设置弹幕透明度 358 | self.textBrowser.optionWidget.opacitySlider.setValue( 359 | self.textSetting[1]) # 设置选项页透明条 360 | self.textBrowser.optionWidget.opacitySlider.value.connect( 361 | self.setDanmuOpacity) 362 | self.setHorizontalPercent(self.textSetting[2]) # 设置横向占比 363 | self.textBrowser.optionWidget.horizontalCombobox.setCurrentIndex( 364 | self.textSetting[2]) # 设置选项页占比框 365 | self.textBrowser.optionWidget.horizontalCombobox.currentIndexChanged.connect( 366 | self.setHorizontalPercent) 367 | self.setVerticalPercent(self.textSetting[3]) # 设置横向占比 368 | self.textBrowser.optionWidget.verticalCombobox.setCurrentIndex( 369 | self.textSetting[3]) # 设置选项页占比框 370 | self.textBrowser.optionWidget.verticalCombobox.currentIndexChanged.connect( 371 | self.setVerticalPercent) 372 | self.setTranslateBrowser(self.textSetting[4]) 373 | self.textBrowser.optionWidget.translateCombobox.setCurrentIndex( 374 | self.textSetting[4]) # 设置同传窗口 375 | self.textBrowser.optionWidget.translateCombobox.currentIndexChanged.connect( 376 | self.setTranslateBrowser) 377 | self.setTranslateFilter(self.textSetting[5]) # 同传过滤字符 378 | self.textBrowser.optionWidget.translateFitler.setText( 379 | self.textSetting[5]) 380 | self.textBrowser.optionWidget.translateFitler.textChanged.connect( 381 | self.setTranslateFilter) 382 | self.textBrowser.closeSignal.connect(self.closeDanmu) 383 | 384 | # self.translator = TextBrowser(self, self.id, '同传') 385 | # self.translator.closeSignal.connect(self.closeTranslator) 386 | 387 | self.danmu = remoteThread(self.roomID) 388 | 389 | self.resizeTimer = QTimer() 390 | self.resizeTimer.timeout.connect(self.resizeVideoItem) 391 | 392 | self.fullScreenTimer = QTimer() 393 | self.fullScreenTimer.timeout.connect(self.hideFrame) 394 | 395 | def setDanmuOpacity(self, value): 396 | if value < 7: 397 | value = 7 # 最小透明度 398 | self.textSetting[1] = value # 记录设置 399 | value = int(value / 101 * 256) 400 | color = str(hex(value))[2:] + '000000' 401 | self.textBrowser.textBrowser.setStyleSheet( 402 | 'background-color:#%s' % color) 403 | self.textBrowser.transBrowser.setStyleSheet( 404 | 'background-color:#%s' % color) 405 | 406 | def setHorizontalPercent(self, index): # 设置弹幕框水平宽度 407 | self.textSetting[2] = index 408 | self.horiPercent = [0.1, 0.15, 0.2, 0.25, 0.3, 409 | 0.35, 0.4, 0.45, 0.5][index] # 记录横向占比 410 | width = self.width() * self.horiPercent 411 | self.textBrowser.resize(width, self.textBrowser.height()) 412 | if width > 300: 413 | self.textBrowser.textBrowser.setFont( 414 | QFont('Microsoft JhengHei', 20, QFont.Bold)) 415 | self.textBrowser.transBrowser.setFont( 416 | QFont('Microsoft JhengHei', 20, QFont.Bold)) 417 | elif 100 < width <= 300: 418 | self.textBrowser.textBrowser.setFont( 419 | QFont('Microsoft JhengHei', width // 20 + 5, QFont.Bold)) 420 | self.textBrowser.transBrowser.setFont( 421 | QFont('Microsoft JhengHei', width // 20 + 5, QFont.Bold)) 422 | else: 423 | self.textBrowser.textBrowser.setFont( 424 | QFont('Microsoft JhengHei', 10, QFont.Bold)) 425 | self.textBrowser.transBrowser.setFont( 426 | QFont('Microsoft JhengHei', 10, QFont.Bold)) 427 | self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) 428 | self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) 429 | 430 | def setVerticalPercent(self, index): # 设置弹幕框垂直高度 431 | self.textSetting[3] = index 432 | self.vertPercent = [0.5, 0.55, 0.6, 0.65, 0.7, 433 | 0.75, 0.8, 0.85, 0.9, 0.95, 1][index] # 记录纵向占比 434 | self.textBrowser.resize(self.textBrowser.width(), 435 | self.height() * self.vertPercent) 436 | self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) 437 | self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) 438 | 439 | def setTranslateBrowser(self, index): 440 | self.textSetting[4] = index 441 | # if not index: 442 | # self.textBrowser.transBrowser.setFixedHeight(self.textBrowser.height() / 3) 443 | # else: 444 | # print(1) 445 | # self.textBrowser.transBrowser.setFixedHeight(0) 446 | self.textBrowser.transBrowser.show( 447 | ) if not index else self.textBrowser.transBrowser.hide() # 显示/隐藏同传 448 | self.textBrowser.adjustSize() 449 | self.resize(self.width() * self.horiPercent, 450 | self.height() * self.vertPercent) 451 | 452 | def setTranslateFilter(self, filterWords): 453 | self.filters = filterWords.split(' ') 454 | 455 | def enterEvent(self, QEvent): 456 | self.topLabel.show() 457 | self.frame.show() 458 | if self.fullScreen: # 如果全屏模式 等待一段时间后隐藏控制条 459 | self.fullScreenTimer.start(3000) 460 | 461 | def leaveEvent(self, QEvent): 462 | self.topLabel.hide() 463 | self.frame.hide() 464 | 465 | def mouseDoubleClickEvent(self, QMouseEvent): 466 | if not self.top: # 非弹出类悬浮窗 467 | self.popWindow.emit([self.id, self.roomID, self.quality, True]) 468 | self.mediaPlay(1) # 暂停播放 469 | 470 | def closeEvent(self, QCloseEvent): # 这个closeEvent只是给悬浮窗用的 471 | self.player.setMedia(QMediaContent(QUrl(''))) 472 | self.danmu.terminate() 473 | self.danmu.quit() 474 | 475 | def hideFrame(self): 476 | self.fullScreenTimer.stop() 477 | self.topLabel.hide() 478 | self.frame.hide() 479 | 480 | def mousePressEvent(self, QMouseEvent): # 设置drag事件 发送拖动封面的房间号 481 | if QMouseEvent.button() == Qt.LeftButton: 482 | drag = QDrag(self) 483 | mimeData = QMimeData() 484 | mimeData.setText('exchange:%s:%s' % (self.id, self.roomID)) 485 | drag.setMimeData(mimeData) 486 | drag.exec_() 487 | 488 | def dropFile(self, text): 489 | if 'roomID' in text: # 从cover拖拽新直播间 490 | self.roomID = int(text.split(':')[1]) 491 | self.addMedia.emit([self.id, self.roomID]) 492 | self.mediaReload() 493 | self.textBrowser.textBrowser.clear() 494 | self.textBrowser.transBrowser.clear() 495 | elif 'exchange' in text: # 交换窗口 496 | fromID, fromRoomID = map(int, text.split(':')[1:]) 497 | if fromID != self.id: 498 | self.exchangeMedia.emit( 499 | [self.id, fromRoomID, fromID, self.roomID]) 500 | self.roomID = fromRoomID 501 | self.mediaReload() 502 | # self.textBrowser.textBrowser.clear() 503 | # self.translator.textBrowser.clear() 504 | 505 | def rightMouseClicked(self, event): 506 | menu = QMenu() 507 | openBrowser = menu.addAction('打开直播间') 508 | chooseQuality = menu.addMenu('选择画质') 509 | originQuality = chooseQuality.addAction('原画') 510 | if self.quality == 10000: 511 | originQuality.setIcon(self.style().standardIcon( 512 | QStyle.SP_DialogApplyButton)) 513 | bluerayQuality = chooseQuality.addAction('蓝光') 514 | if self.quality == 400: 515 | bluerayQuality.setIcon( 516 | self.style().standardIcon(QStyle.SP_DialogApplyButton)) 517 | highQuality = chooseQuality.addAction('超清') 518 | if self.quality == 250: 519 | highQuality.setIcon(self.style().standardIcon( 520 | QStyle.SP_DialogApplyButton)) 521 | lowQuality = chooseQuality.addAction('流畅') 522 | if self.quality == 80: 523 | lowQuality.setIcon(self.style().standardIcon( 524 | QStyle.SP_DialogApplyButton)) 525 | if not self.top: # 非弹出类悬浮窗 526 | popWindow = menu.addAction('悬浮窗播放') 527 | else: 528 | opacityMenu = menu.addMenu('调节透明度') 529 | percent100 = opacityMenu.addAction('100%') 530 | if self.opacity == 100: 531 | percent100.setIcon(self.style().standardIcon( 532 | QStyle.SP_DialogApplyButton)) 533 | percent80 = opacityMenu.addAction('80%') 534 | if self.opacity == 80: 535 | percent80.setIcon(self.style().standardIcon( 536 | QStyle.SP_DialogApplyButton)) 537 | percent60 = opacityMenu.addAction('60%') 538 | if self.opacity == 60: 539 | percent60.setIcon(self.style().standardIcon( 540 | QStyle.SP_DialogApplyButton)) 541 | percent40 = opacityMenu.addAction('40%') 542 | if self.opacity == 40: 543 | percent40.setIcon(self.style().standardIcon( 544 | QStyle.SP_DialogApplyButton)) 545 | percent20 = opacityMenu.addAction('20%') 546 | if self.opacity == 20: 547 | percent20.setIcon(self.style().standardIcon( 548 | QStyle.SP_DialogApplyButton)) 549 | action = menu.exec_(self.mapToGlobal(event.pos())) 550 | if action == openBrowser: 551 | if self.roomID: 552 | QDesktopServices.openUrl( 553 | QUrl(r'https://live.bilibili.com/%s' % self.roomID)) 554 | elif action == originQuality: 555 | self.changeQuality.emit([self.id, 10000]) 556 | self.quality = 10000 557 | self.mediaReload() 558 | elif action == bluerayQuality: 559 | self.changeQuality.emit([self.id, 400]) 560 | self.quality = 400 561 | self.mediaReload() 562 | elif action == highQuality: 563 | self.changeQuality.emit([self.id, 250]) 564 | self.quality = 250 565 | self.mediaReload() 566 | elif action == lowQuality: 567 | self.changeQuality.emit([self.id, 80]) 568 | self.quality = 80 569 | self.mediaReload() 570 | if not self.top: 571 | if action == popWindow: 572 | self.popWindow.emit( 573 | [self.id, self.roomID, self.quality, False]) 574 | self.mediaPlay(1) # 暂停播放 575 | elif self.top: 576 | if action == percent100: 577 | self.setWindowOpacity(1) 578 | self.opacity = 100 579 | elif action == percent80: 580 | self.setWindowOpacity(0.8) 581 | self.opacity = 80 582 | elif action == percent60: 583 | self.setWindowOpacity(0.6) 584 | self.opacity = 60 585 | elif action == percent40: 586 | self.setWindowOpacity(0.4) 587 | self.opacity = 40 588 | elif action == percent20: 589 | self.setWindowOpacity(0.2) 590 | self.opacity = 20 591 | 592 | def resizeEvent(self, QEvent): 593 | self.scene.setSceneRect(1, 1, self.width() - 2, self.height() - 2) 594 | width = self.width() * self.horiPercent 595 | self.textBrowser.resize(width, self.height() * self.vertPercent) 596 | if width > 300: 597 | self.textBrowser.textBrowser.setFont( 598 | QFont('Microsoft JhengHei', 20, QFont.Bold)) 599 | self.textBrowser.transBrowser.setFont( 600 | QFont('Microsoft JhengHei', 20, QFont.Bold)) 601 | elif 100 < width <= 300: 602 | self.textBrowser.textBrowser.setFont( 603 | QFont('Microsoft JhengHei', width // 20 + 5, QFont.Bold)) 604 | self.textBrowser.transBrowser.setFont( 605 | QFont('Microsoft JhengHei', width // 20 + 5, QFont.Bold)) 606 | else: 607 | self.textBrowser.textBrowser.setFont( 608 | QFont('Microsoft JhengHei', 10, QFont.Bold)) 609 | self.textBrowser.transBrowser.setFont( 610 | QFont('Microsoft JhengHei', 10, QFont.Bold)) 611 | # if not self.textBrowser.transBrowser.isHidden(): 612 | # self.textBrowser.transBrowser.setFixedHeight(self.textBrowser.height() / 3) 613 | self.textBrowser.move(0, 0) 614 | self.textBrowser.textBrowser.verticalScrollBar().setValue(100000000) 615 | self.textBrowser.transBrowser.verticalScrollBar().setValue(100000000) 616 | self.resizeTimer.start(50) # 延迟50ms修改video窗口 否则容易崩溃 617 | 618 | def resizeVideoItem(self): 619 | self.resizeTimer.stop() 620 | self.videoItem.setSize(QSizeF(self.width(), self.height())) 621 | 622 | def setVolume(self, value): 623 | self.player.setVolume(value) 624 | self.volumeChanged.emit([self.id, value]) 625 | 626 | def closeDanmu(self): 627 | self.textSetting[0] = False 628 | # self.setDanmu.emit([self.id, False]) # 旧版信号 已弃用 629 | 630 | def closeTranslator(self): 631 | self.setTranslator.emit([self.id, False]) 632 | 633 | def showDanmu(self): 634 | if self.textBrowser.isHidden(): 635 | self.textBrowser.show() 636 | # self.translator.show() 637 | else: 638 | self.textBrowser.hide() 639 | # self.translator.hide() 640 | self.textSetting[0] = not self.textBrowser.isHidden() 641 | # self.setDanmu.emit([self.id, not self.textBrowser.isHidden()]) 642 | # self.setTranslator.emit([self.id, not self.translator.isHidden()]) 643 | 644 | def mediaPlay(self, force=0): 645 | if force == 1: 646 | self.player.pause() 647 | self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) 648 | elif force == 2: 649 | self.player.play() 650 | self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) 651 | elif self.player.state() == 1: 652 | self.player.pause() 653 | self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) 654 | elif self.player.state() == 2: 655 | self.player.play() 656 | self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) 657 | 658 | def mediaMute(self, force=0, emit=True): 659 | if force == 1: 660 | self.player.setMuted(False) 661 | self.volume.setIcon( 662 | self.style().standardIcon(QStyle.SP_MediaVolume)) 663 | elif force == 2: 664 | self.player.setMuted(True) 665 | self.volume.setIcon(self.style().standardIcon( 666 | QStyle.SP_MediaVolumeMuted)) 667 | elif self.player.isMuted(): 668 | self.player.setMuted(False) 669 | self.volume.setIcon( 670 | self.style().standardIcon(QStyle.SP_MediaVolume)) 671 | else: 672 | self.player.setMuted(True) 673 | self.volume.setIcon(self.style().standardIcon( 674 | QStyle.SP_MediaVolumeMuted)) 675 | if emit: 676 | self.mutedChanged.emit([self.id, self.player.isMuted()]) 677 | 678 | def mediaReload(self): 679 | if self.roomID: 680 | # self.player.stop() 681 | self.getMediaURL.setConfig(self.roomID, self.quality) # 设置房号和画质 682 | self.getMediaURL.start() 683 | else: 684 | self.mediaStop() 685 | 686 | def mediaStop(self): 687 | self.roomID = 0 688 | self.url = QMediaContent(QUrl('')) 689 | self.topLabel.setText(' 窗口%s 未定义的直播间' % (self.id + 1)) 690 | self.titleLabel.setText('未定义') 691 | # self.player.stop() 692 | self.player.setMedia(self.url) 693 | self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay)) 694 | self.deleteMedia.emit(self.id) 695 | try: 696 | self.danmu.message.disconnect(self.playDanmu) 697 | except: 698 | pass 699 | self.danmu.terminate() 700 | self.danmu.quit() 701 | self.danmu.wait() 702 | 703 | def setMedia(self, qurl): 704 | self.setTitle() 705 | self.play.setIcon(self.style().standardIcon(QStyle.SP_MediaPause)) 706 | self.danmu.setRoomID(str(self.roomID)) 707 | try: 708 | self.danmu.message.disconnect(self.playDanmu) 709 | except: 710 | pass 711 | self.danmu.message.connect(self.playDanmu) 712 | self.danmu.terminate() 713 | self.danmu.start() 714 | self.player.setMedia(qurl) 715 | self.player.play() 716 | 717 | def setTitle(self): 718 | if not self.roomID: 719 | title = '未定义的直播间' 720 | uname = '未定义' 721 | else: 722 | r = requests.get( 723 | r'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom?room_id=%s' % self.roomID) 724 | data = json.loads(r.text) 725 | if data['message'] == '房间已加密': 726 | title = '房间已加密' 727 | uname = '房号: %s' % self.roomID 728 | elif not data['data']: 729 | title = '房间好像不见了-_-?' 730 | uname = '未定义' 731 | else: 732 | data = data['data'] 733 | liveStatus = data['room_info']['live_status'] 734 | title = data['room_info']['title'] 735 | uname = data['anchor_info']['base_info']['uname'] 736 | if liveStatus != 1: 737 | uname = '(未开播)' + uname 738 | self.topLabel.setText(' 窗口%s %s' % (self.id + 1, title)) 739 | self.titleLabel.setText(uname) 740 | 741 | def playDanmu(self, message): 742 | if self.textBrowser.transBrowser.isHidden(): 743 | self.textBrowser.textBrowser.append(message) 744 | else: 745 | token = False 746 | for symbol in self.filters: 747 | if symbol in message: 748 | self.textBrowser.transBrowser.append(message) 749 | token = True 750 | break 751 | if not token: 752 | self.textBrowser.textBrowser.append(message) 753 | -------------------------------------------------------------------------------- /danmu.py: -------------------------------------------------------------------------------- 1 | """将弹幕机分离出来单独开发 2 | """ 3 | from PyQt5.QtWidgets import QLabel, QToolButton, QWidget, QComboBox, QLineEdit, QTextBrowser, QGridLayout, QStyle 4 | from PyQt5.QtGui import QFont 5 | from PyQt5.QtCore import Qt, pyqtSignal, QPoint 6 | from CommonWidget import Slider 7 | 8 | 9 | class Bar(QLabel): 10 | """自定义标题栏""" 11 | moveSignal = pyqtSignal(QPoint) 12 | 13 | def __init__(self, text): 14 | super(Bar, self).__init__() 15 | self.setText(text) 16 | self.setFixedHeight(25) 17 | 18 | def mousePressEvent(self, event): 19 | self.startPos = event.pos() 20 | 21 | def mouseMoveEvent(self, event): 22 | self.moveSignal.emit(self.mapToParent(event.pos() - self.startPos)) 23 | 24 | 25 | class ToolButton(QToolButton): 26 | """标题栏按钮""" 27 | 28 | def __init__(self, icon): 29 | super(ToolButton, self).__init__() 30 | self.setStyleSheet('border-color:#CCCCCC') 31 | self.setFixedSize(25, 25) 32 | self.setIcon(icon) 33 | 34 | 35 | class TextOpation(QWidget): 36 | """弹幕机选项 - 弹出式窗口""" 37 | 38 | def __init__(self, setting=[50, 1, 7, 0, '【 [ {', 10, 0]): 39 | super(TextOpation, self).__init__() 40 | self.resize(300, 300) 41 | self.setWindowTitle('弹幕窗设置') 42 | self.setWindowFlag(Qt.WindowStaysOnTopHint) 43 | 44 | # ---- 窗体布局 ---- 45 | layout = QGridLayout(self) 46 | layout.addWidget(QLabel('字体大小'), 0, 0, 1, 1) 47 | self.fontSizeCombox = QComboBox() 48 | self.fontSizeCombox.addItems([str(i) for i in range(5, 26)]) 49 | self.fontSizeCombox.setCurrentIndex(setting[5]) 50 | layout.addWidget(self.fontSizeCombox, 0, 1, 1, 1) 51 | 52 | layout.addWidget(QLabel('窗体透明度'), 1, 0, 1, 1) 53 | self.opacitySlider = Slider() 54 | self.opacitySlider.setValue(setting[0]) 55 | layout.addWidget(self.opacitySlider, 1, 1, 1, 1) 56 | 57 | layout.addWidget(QLabel('窗体横向占比'), 2, 0, 1, 1) 58 | self.horizontalCombobox = QComboBox() 59 | self.horizontalCombobox.addItems( 60 | ['%d' % x + '%' for x in range(10, 110, 10)]) 61 | self.horizontalCombobox.setCurrentIndex(setting[1]) 62 | layout.addWidget(self.horizontalCombobox, 2, 1, 1, 1) 63 | 64 | layout.addWidget(QLabel('窗体纵向占比'), 3, 0, 1, 1) 65 | self.verticalCombobox = QComboBox() 66 | self.verticalCombobox.addItems( 67 | ['%d' % x + '%' for x in range(10, 110, 10)]) 68 | self.verticalCombobox.setCurrentIndex(setting[2]) 69 | layout.addWidget(self.verticalCombobox, 3, 1, 1, 1) 70 | 71 | layout.addWidget(QLabel('弹幕窗类型'), 4, 0, 1, 1) 72 | self.translateCombobox = QComboBox() 73 | self.translateCombobox.addItems(['弹幕和同传', '只显示弹幕', '只显示同传']) 74 | self.translateCombobox.setCurrentIndex(setting[3]) 75 | layout.addWidget(self.translateCombobox, 4, 1, 1, 1) 76 | 77 | layout.addWidget(QLabel('同传过滤字符 (空格隔开)'), 5, 0, 1, 1) 78 | self.translateFitler = QLineEdit('') 79 | self.translateFitler.setText(setting[4]) 80 | self.translateFitler.setFixedWidth(100) 81 | layout.addWidget(self.translateFitler, 5, 1, 1, 1) 82 | 83 | layout.addWidget(QLabel('礼物和进入信息'), 6, 0, 1, 1) 84 | self.showEnterRoom = QComboBox() 85 | self.showEnterRoom.addItems(['显示礼物和进入信息', '只显示礼物', '只显示进入信息', '隐藏窗口']) 86 | self.showEnterRoom.setCurrentIndex(setting[6]) 87 | layout.addWidget(self.showEnterRoom, 6, 1, 1, 1) 88 | 89 | 90 | class TextBrowser(QWidget): 91 | """弹幕机 - 弹出式窗口 92 | 通过限制移动位置来模拟嵌入式窗口 93 | """ 94 | closeSignal = pyqtSignal() 95 | moveSignal = pyqtSignal(QPoint) 96 | 97 | def __init__(self, parent): 98 | super(TextBrowser, self).__init__(parent) 99 | self.optionWidget = TextOpation() 100 | self.setWindowTitle('弹幕机') 101 | self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) 102 | self.setAttribute(Qt.WA_TranslucentBackground) 103 | 104 | # ---- 窗体布局 ---- 105 | layout = QGridLayout(self) 106 | layout.setContentsMargins(0, 0, 0, 0) 107 | layout.setSpacing(0) 108 | 109 | # 标题栏 110 | self.bar = Bar(' 弹幕机') 111 | self.bar.setStyleSheet('background:#AAAAAAAA') 112 | self.bar.moveSignal.connect(self.moveWindow) 113 | layout.addWidget(self.bar, 0, 0, 1, 10) 114 | # 弹幕选项菜单 115 | self.optionButton = ToolButton( 116 | self.style().standardIcon(QStyle.SP_FileDialogDetailedView)) 117 | self.optionButton.clicked.connect(self.optionWidget.show) # 弹出设置菜单 118 | layout.addWidget(self.optionButton, 0, 8, 1, 1) 119 | # 关闭按钮 120 | self.closeButton = ToolButton( 121 | self.style().standardIcon(QStyle.SP_TitleBarCloseButton)) 122 | self.closeButton.clicked.connect(self.userClose) 123 | layout.addWidget(self.closeButton, 0, 9, 1, 1) 124 | 125 | # 弹幕区域 126 | self.textBrowser = QTextBrowser() 127 | self.textBrowser.setFont(QFont('Microsoft JhengHei', 14, QFont.Bold)) 128 | self.textBrowser.setStyleSheet('border-width:1') 129 | # textCursor = self.textBrowser.textCursor() 130 | # textBlockFormat = QTextBlockFormat() 131 | # textBlockFormat.setLineHeight(17, QTextBlockFormat.FixedHeight) # 弹幕框行距 132 | # textCursor.setBlockFormat(textBlockFormat) 133 | # self.textBrowser.setTextCursor(textCursor) 134 | layout.addWidget(self.textBrowser, 1, 0, 1, 10) 135 | 136 | # 同传区域 137 | self.transBrowser = QTextBrowser() 138 | self.transBrowser.setFont(QFont('Microsoft JhengHei', 14, QFont.Bold)) 139 | self.transBrowser.setStyleSheet('border-width:1') 140 | layout.addWidget(self.transBrowser, 2, 0, 1, 10) 141 | 142 | # 信息区域 143 | self.msgsBrowser = QTextBrowser() 144 | self.msgsBrowser.setFont(QFont('Microsoft JhengHei', 14, QFont.Bold)) 145 | self.msgsBrowser.setStyleSheet('border-width:1') 146 | # self.msgsBrowser.setMaximumHeight(100) 147 | layout.addWidget(self.msgsBrowser, 3, 0, 1, 10) 148 | 149 | def userClose(self): 150 | self.hide() 151 | self.closeSignal.emit() 152 | 153 | def moveWindow(self, moveDelta): 154 | self.moveSignal.emit(self.pos() + moveDelta) 155 | -------------------------------------------------------------------------------- /docs/live-api.md: -------------------------------------------------------------------------------- 1 | 直播相关接口 2 | ============ 3 | 4 | > 大致描述了接入一个直播平台需要的接口 5 | 6 | ## 需求 7 | 8 | + **获取直播推流** 9 | + 直播相关信息 10 | + 直播状态 11 | + 封面 12 | + 标题 13 | + 房间uid 14 | + 头像 15 | + 弹幕/sc 16 | + 直播间添加 17 | + 获取分区热榜 18 | + 导入关注 19 | 20 | 21 | ## Bili 22 | 23 | 接口实现:[Passkou/bilibili_api: 哔哩哔哩的API调用模块]( https://github.com/Passkou/bilibili_api ) 24 | 25 | 参考文档: 26 | + [SocialSisterYi/bilibili-API-collect: 哔哩哔哩-API收集整理]( https://github.com/SocialSisterYi/bilibili-API-collect ) 27 | + [lovelyyoshino/Bilibili-Live-API: BILIBILI 直播/番剧 API]( https://github.com/lovelyyoshino/Bilibili-Live-API ) 28 | -------------------------------------------------------------------------------- /docs/vlc-player.md: -------------------------------------------------------------------------------- 1 | VLC 播放器调用 2 | ============== 3 | 4 | 仅描述在某个 QFrame 中调用 VLC 播放器的方法。 5 | 6 | 7 | ## VLC 实例化 8 | 9 | ```python3 10 | self.instance = vlc.Instance() 11 | ``` 12 | 13 | ## Player 实例化 14 | ```python3 15 | self.player = self.instance.media_player_new() 16 | 17 | # 更多 self.player 的设置 ... 18 | 19 | # 绑定到 QFrame 20 | if platform.system() == "Linux": # for Linux using the X Server 21 | self.mediaplayer.set_xwindow(int(self.videoframe.winId())) 22 | elif platform.system() == "Windows": # for Windows 23 | self.mediaplayer.set_hwnd(int(self.videoframe.winId())) 24 | elif platform.system() == "Darwin": # for MacOS 25 | self.mediaplayer.set_nsobject(int(self.videoframe.winId())) 26 | 27 | # 设置视频流来源 28 | self.mediaplayer.set_media(self.media) 29 | ``` 30 | 31 | ## `self.player.stop()` 的问题 32 | `stop()` 之后 `self.player` 被设置为停止状态[^1][src-libvlc_media_player_stop]。 33 | 再次启用时会出现无法单独控制音频的bug。 34 | 因此需要重新实例化。 35 | 36 | 官方的qt例子也是采用先 stop 然后重新实例化的方式[^2][src-qtplayer]。 37 | 38 | 现在采用将 `stop()` 和重新实例化封装为一个函数的办法解决以上bug。 39 | 参见:`VideoWidget.playerRestart()` 40 | 41 | 42 | [src-libvlc_media_player_stop]: https://code.videolan.org/videolan/vlc/-/blob/3.0.0-git/lib/media_player.c#L1045 43 | [src-qtplayer]: https://code.videolan.org/videolan/vlc/-/blob/3.0.0-git/doc/libvlc/QtPlayer/player.cpp#L119 44 | 45 | 46 | ## 参考资料 47 | + [doc/libvlc/QtPlayer/player.cpp · 3.0.0-git · VideoLAN / VLC · GitLab]( https://code.videolan.org/videolan/vlc/-/blob/3.0.0-git/doc/libvlc/QtPlayer/player.cpp ) 48 | VLC 官方的 Qt 例子 49 | + [python-vlc/pyqt5vlc.py at master · oaubert/python-vlc]( https://github.com/oaubert/python-vlc/blob/master/examples/pyqt5vlc.py ) 50 | python-vlc 的 PyQt 例子 51 | -------------------------------------------------------------------------------- /docs/vlc-plugins.md: -------------------------------------------------------------------------------- 1 | python-vlc 二进制依赖说明 2 | ========================== 3 | 4 | 5 | ## Plugin 必备插件 6 | 7 | 播放一个音视频分为4个步骤 8 | 9 | ### access 访问 10 | 目前的逻辑是先缓存到文件,然后直接播放文件。 11 | 故仅保留文件系统的读取模块。 12 | 13 | 如果要增加其他视频流来源,则需补充对应的插件。(建议直接安装完整版 VLC) 14 | 15 | + access/libfilesystem_plugin 16 | 17 | ### demux 解复用 18 | 文件为 flv 格式,貌似无需解复用。 19 | 20 | ### decode 解码 21 | + codec/libdxva2_plugin.dll 22 | + codec/libavcodec_plugin.dll 23 | 24 | ### output 输出 25 | **音频** 26 | + audio_output/libdirectsound_plugin.dll 27 | ds 音频输出 28 | + audio_mixer/libfloat_mixer_plugin.dll 29 | 浮点数音量。或许可以去掉,取决于音量混合使用的方法 30 | 31 | **视频** 32 | + video_chroma/libswscale_plugin.dll 33 | 使用 YUV420 == i420 34 | + video_output/libdirect3d_plugin.dll 35 | + video_output/libdrawable_plugin.dll 36 | + video_output/libdirectdraw_plugin.dll 37 | 硬件加速非必须? 38 | 39 | 40 | ## 参考资料 41 | 42 | + plugin 文件夹的源码: [modules · master · VideoLAN / VLC · GitLab]( https://code.videolan.org/videolan/vlc/-/tree/master/modules ) 43 | 不知道某个动态链接库的功能时,去看源码开头的说明。 44 | 45 | + [VLC源码分析]( https://www.dazhuanlan.com/2020/02/24/5e53abbc6d6fe/ ) 46 | + [VLC框架总结(一)VLC源码及各modules功能介绍]( https://blog.csdn.net/hejjunlin/article/details/77888143 ) 47 | 48 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhimingshenjun/DD_Monitor/0e7fe77a370c69a4dec5aefe29ea99f21760c2b1/favicon.ico -------------------------------------------------------------------------------- /hooks/hook-vlc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """PyInstaller python-vlc hook 3 | ref: https://pyinstaller.readthedocs.io/en/stable/hooks.html 4 | """ 5 | from PyInstaller.compat import is_win 6 | from vlc import dll as vlc_dll 7 | from vlc import plugin_path 8 | import os 9 | 10 | 11 | datas = [] 12 | binaries = [] 13 | hiddenimports = [] 14 | plugins_dlls = [ 15 | # (subdir, fname) 16 | ('access', 'libfilesystem_plugin'), 17 | 18 | ('codec', 'libdxva2_plugin'), 19 | ('codec', 'libavcodec_plugin'), 20 | ('audio_output', 'libdirectsound_plugin'), 21 | ('audio_mixer', 'libfloat_mixer_plugin'), 22 | 23 | ('video_chroma', 'libswscale_plugin'), 24 | ('video_output', 'libdirect3d11_plugin'), 25 | ('video_output', 'libdrawable_plugin'), 26 | ('video_output', 'libdirectdraw_plugin'), 27 | ] 28 | 29 | 30 | def gen_plugins_binary(vlc_root, dest, fnames=plugins_dlls, ext='.dll', use_subdir=True): 31 | plugin_binaries = [] 32 | for (subdir, fname) in fnames: 33 | # plugin_path: 存放插件的目录 34 | # win: 'VLC/plugins/' 35 | # mac: 'VLC.app/Contents/MacOS/plugins/' 36 | if use_subdir: 37 | plugin_root = os.path.join(vlc_root, 'plugins', subdir) 38 | else: 39 | plugin_root = os.path.join(vlc_root, 'plugins') 40 | 41 | lib_path = os.path.join(plugin_root, fname + ext) 42 | if not os.path.isfile(lib_path): 43 | import warnings 44 | warnings.warn(f'VLC plugin {fname} not found!', ResourceWarning) 45 | continue 46 | 47 | plugin_binaries.append((lib_path, dest)) 48 | 49 | return plugin_binaries 50 | 51 | 52 | if is_win: 53 | binaries = [ 54 | (str(vlc_dll._name), '.'), 55 | # (os.path.join(plugin_path, 'libvlc.dll'), '.'), 56 | (os.path.join(plugin_path, 'libvlccore.dll'), '.'), 57 | ] 58 | binaries += gen_plugins_binary(plugin_path, 'plugins') 59 | # end is_win 60 | -------------------------------------------------------------------------------- /log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """全局日志 3 | Note: 仅在入口文件中导入一次 4 | """ 5 | import os 6 | import datetime 7 | import logging 8 | import sys 9 | 10 | 11 | def get_submod_log(submod_name): 12 | return logging.getLogger('Main' + '.' + submod_name) 13 | 14 | 15 | class LoggerStream(object): 16 | """假 stream,将 stdout/stderr 流重定向到日志,避免win上无头模式运行时报错。 17 | ref: https://docs.python.org/3/library/sys.html#sys.__stdout__ 18 | """ 19 | def __init__(self, name, level, fileno): 20 | """ 21 | :param logger: 日志实例 22 | :param level: 日志等级 23 | """ 24 | self.logger = get_submod_log(name) 25 | self.level = level 26 | self.fileno = fileno 27 | 28 | def fileno(self): 29 | return self.fileno 30 | 31 | def write(self, lines): 32 | for line in lines.splitlines(): 33 | self.logger.log(self.level, line) 34 | 35 | def flush(self): 36 | for handler in self.logger.handlers: 37 | handler.flush() 38 | 39 | 40 | def init_log(application_path): 41 | log_path = os.path.join(application_path, r'logs/log-%s.txt' % (datetime.datetime.today().strftime('%Y-%m-%d'))) 42 | if sys.stderr: 43 | get_submod_log('log').addHandler(logging.StreamHandler()) 44 | logging.basicConfig( 45 | level=logging.INFO, 46 | format="%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s", 47 | handlers=[ 48 | logging.FileHandler(log_path,'w','utf-8'), 49 | logging.StreamHandler() 50 | ] 51 | ) 52 | else: 53 | logging.basicConfig( 54 | level=logging.INFO, 55 | format="%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s", 56 | filename=log_path, 57 | filemode='a' 58 | ) 59 | sys.stdout = LoggerStream('STDOUT', logging.INFO, 1) 60 | sys.stderr = LoggerStream('STDERR', logging.ERROR, 2) 61 | 62 | 63 | -------------------------------------------------------------------------------- /pay.py: -------------------------------------------------------------------------------- 1 | """ 2 | 赞助页弹窗 3 | """ 4 | import requests 5 | from PyQt5.QtWidgets import * # QAction,QFileDialog 6 | from PyQt5.QtGui import * # QIcon,QPixmap 7 | from PyQt5.QtCore import * # QSize 8 | 9 | 10 | class DownloadImage(QThread): 11 | """下载图片 - 二维码""" 12 | img = pyqtSignal(QPixmap) 13 | 14 | def __init__(self): 15 | super(DownloadImage, self).__init__() 16 | 17 | def run(self): 18 | try: 19 | r = requests.get(r'https://i0.hdslb.com/bfs/album/a4d2644425634cb8568570b77f4ba45f2b84fe67.png') 20 | img = QPixmap.fromImage(QImage.fromData(r.content)) 21 | self.img.emit(img) 22 | except Exception as e: 23 | print(str(e)) 24 | 25 | 26 | class thankToBoss(QThread): 27 | """获取感谢名单""" 28 | bossList = pyqtSignal(list) 29 | 30 | def __init__(self, parent=None): 31 | super(thankToBoss, self).__init__(parent) 32 | 33 | def run(self): 34 | headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.221 Safari/537.36 SE 2.X MetaSr 1.0'} 35 | response = requests.get(r'https://github.com/jiafangjun/DD_KaoRou2/blob/master/感谢石油王.csv', headers=headers) 36 | bossList = [] 37 | html = response.text.split('\n') 38 | for cnt, line in enumerate(html): 39 | if 'RMB<' in line: 40 | boss = html[cnt - 1].split('>')[1].split('<')[0] 41 | rmb = line.split('>')[1].split('<')[0] 42 | bossList.append([boss, rmb]) 43 | if bossList: 44 | self.bossList.emit(bossList) 45 | else: 46 | self.bossList.emit([['名单列表获取失败', '']]) 47 | 48 | 49 | class pay(QDialog): 50 | """投喂弹窗""" 51 | def __init__(self): 52 | super().__init__() 53 | self.setWindowTitle('赞助和支持') 54 | self.resize(564, 500) 55 | layout = QGridLayout() 56 | self.setLayout(layout) 57 | txt = u'DD监控室由B站up:神君Channel 业余时间独立开发制作。\n\ 58 | \n所有功能全部永久免费给广大DD使用\n\ 59 | \n有独立经济来源的老板们如觉得监控室好用的话,不妨小小支持亿下\n\ 60 | \n一元也是对我继续更新监控室的莫大鼓励。十分感谢!\n' 61 | label = QLabel(txt) 62 | label.setTextInteractionFlags(Qt.TextSelectableByMouse) 63 | label.setAlignment(Qt.AlignCenter) 64 | layout.addWidget(label, 0, 0, 1, 2) 65 | 66 | self.QR = QLabel() 67 | layout.addWidget(self.QR, 1, 0, 1, 1) 68 | 69 | self.bossTable = QTableWidget() 70 | self.bossTable.setEditTriggers(QAbstractItemView.NoEditTriggers) 71 | self.bossTable.setRowCount(3) 72 | self.bossTable.setColumnCount(2) 73 | for i in range(2): 74 | self.bossTable.setColumnWidth(i, 105) 75 | self.bossTable.setHorizontalHeaderLabels(['石油王', '投喂了']) 76 | self.bossTable.setItem(0, 0, QTableWidgetItem('石油王鸣谢名单')) 77 | self.bossTable.setItem(0, 1, QTableWidgetItem('正在获取...')) 78 | layout.addWidget(self.bossTable, 1, 1, 1, 1) 79 | 80 | self.getQR = DownloadImage() 81 | self.getQR.img.connect(self.updateQR) 82 | self.getQR.start() 83 | 84 | self.thankToBoss = thankToBoss() 85 | self.thankToBoss.bossList.connect(self.updateBossList) 86 | # self.thankToBoss.start() 87 | 88 | def updateQR(self, img): 89 | self.QR.setPixmap(img) 90 | 91 | def updateBossList(self, bossList): 92 | self.bossTable.clear() 93 | self.bossTable.setColumnCount(2) 94 | self.bossTable.setRowCount(len(bossList)) 95 | if len(bossList) > 3: 96 | biggestBossList = [] 97 | for _ in range(3): 98 | sc = 0 99 | for cnt, i in enumerate(bossList): 100 | money = float(i[1].split(' ')[0]) 101 | if money > sc: 102 | sc = money 103 | bossNum = cnt 104 | biggestBossList.append(bossList.pop(bossNum)) 105 | for y, i in enumerate(biggestBossList): 106 | self.bossTable.setItem(y, 0, QTableWidgetItem(i[0])) 107 | self.bossTable.setItem(y, 1, QTableWidgetItem(i[1])) 108 | self.bossTable.item(y, 0).setTextAlignment(Qt.AlignCenter) 109 | self.bossTable.item(y, 1).setTextAlignment(Qt.AlignCenter) 110 | for y, i in enumerate(bossList): 111 | self.bossTable.setItem(y + 3, 0, QTableWidgetItem(i[0])) 112 | self.bossTable.setItem(y + 3, 1, QTableWidgetItem(i[1])) 113 | self.bossTable.item(y + 3, 0).setTextAlignment(Qt.AlignCenter) 114 | self.bossTable.item(y + 3, 1).setTextAlignment(Qt.AlignCenter) 115 | self.bossTable.setHorizontalHeaderLabels(['石油王', '投喂了']) 116 | -------------------------------------------------------------------------------- /remote.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 通过QThread + websocket获取直播弹幕并返回给播放窗口模块做展示 4 | """ 5 | import asyncio 6 | import zlib 7 | import json 8 | import requests 9 | # from aiowebsocket.converses import AioWebSocket 10 | from PyQt5.QtCore import QThread, pyqtSignal 11 | import logging 12 | from bilibili_api import live 13 | 14 | 15 | class Live(live.LiveDanmaku): 16 | 17 | def __init__(self, room_display_id): 18 | super().__init__(room_display_id) 19 | 20 | def register(self, event: str, func: callable): 21 | self.__getattribute__("_LiveDanmaku__event_handlers")[event].append(func) 22 | 23 | 24 | class remoteThread(QThread): 25 | message = pyqtSignal(str) 26 | 27 | def __init__(self, roomID): 28 | super(remoteThread, self).__init__() 29 | self.roomID = roomID 30 | if len(self.roomID) <= 3: 31 | if self.roomID == '0': 32 | return 33 | headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 \ 34 | (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36 QIHU 360SE'} 35 | html = requests.get('https://live.bilibili.com/' + 36 | self.roomID, headers=headers).text 37 | for line in html.split('\n'): 38 | if '"roomid":' in line: 39 | self.roomID = line.split('"roomid":')[1].split(',')[0] 40 | 41 | async def startup(self): 42 | self.roomID = int(self.roomID) 43 | self.room = Live(self.roomID) 44 | self.room.add_event_listener('DANMU_MSG', self.danmu) # 用户发送弹幕 45 | # self.room.add_event_listener('SEND_GIFT', self.gift) # 礼物 46 | # self.room.add_event_listener('COMBO_SEND', self.combo_gift) # 礼物连击 47 | # self.room.add_event_listener('GUARD_BUY', self.guard) # 续费大航海 48 | self.room.add_event_listener('SUPER_CHAT_MESSAGE', self.sc) # 醒目留言(SC) 49 | # self.room.add_event_listener('INTERACT_WORD', self.enter) # 用户进入直播间 50 | await self.room.connect() 51 | # await asyncio.wait([self.room.connect()]) 52 | 53 | async def danmu(self, jd): 54 | self.message.emit(jd['data']['info'][1]) 55 | 56 | async def gift(self, event): 57 | print(event) 58 | 59 | async def combo_gift(self, event): 60 | print(event) 61 | 62 | async def guard(self, event): 63 | print(event) 64 | 65 | async def sc(self, jd): 66 | jd = jd['data'] 67 | self.message.emit( 68 | f"【SC(¥{jd['data']['price']}) {jd['data']['user_info']['uname']}: {jd['data']['message']}】" 69 | ) 70 | 71 | async def enter(self, event): 72 | print(event) 73 | 74 | def printDM(self, data): 75 | packetLen = int(data[:4].hex(), 16) 76 | ver = int(data[6:8].hex(), 16) 77 | op = int(data[8:12].hex(), 16) 78 | 79 | if len(data) > packetLen: 80 | self.printDM(data[packetLen:]) 81 | data = data[:packetLen] 82 | 83 | if ver == 2: 84 | data = zlib.decompress(data[16:]) 85 | self.printDM(data) 86 | return 87 | 88 | if ver == 1: 89 | if op == 3: 90 | # print('[RENQI] {}'.format(int(data[16:].hex(),16))) 91 | pass 92 | return 93 | 94 | captainName = { 95 | 0: "", 96 | 1: "总督", 97 | 2: "提督", 98 | 3: "舰长" 99 | } 100 | 101 | userType = { 102 | "#FF7C28": "+++", 103 | "#E17AFF": "++", 104 | "#00D1F1": "+", 105 | "": "" 106 | } 107 | 108 | adminType = ["", "*"] 109 | 110 | def getMetal(jd): 111 | try: 112 | medal = [] 113 | if jd['cmd'] == 'DANMU_MSG': 114 | jz = captainName[jd['info'][3][10]] 115 | if jz: 116 | medal.append(jz) 117 | medal.append(jd['info'][3][1]) 118 | medal.append(str(jd['info'][3][0])) 119 | else: 120 | jz = captainName[jd['data']['medal_info']['guard_level']] 121 | if jz: 122 | medal.append(jz) 123 | medal.append(jd['data']['medal_info']['medal_name']) 124 | medal.append(jd['data']['medal_info']['medal_level']) 125 | return "|" + "|".join(medal) + "|" 126 | except: 127 | return "" 128 | 129 | if op == 5: 130 | try: 131 | jd = json.loads(data[16:].decode('utf-8', errors='ignore')) 132 | if jd['cmd'] == 'DANMU_MSG': 133 | self.message.emit( 134 | # f"{userType[jd['info'][2][7]]}{adminType[jd['info'][2][2]]}{getMetal(jd)} {jd['info'][2][1]}: {jd['info'][1]}" 135 | f"{jd['info'][1]}" 136 | ) 137 | elif jd['cmd'] == 'SUPER_CHAT_MESSAGE': 138 | self.message.emit( 139 | f"【SC(¥{jd['data']['price']}) {getMetal(jd)} {jd['data']['user_info']['uname']}: {jd['data']['message']}】" 140 | ) 141 | elif jd['cmd'] == 'SEND_GIFT': 142 | if jd['data']['coin_type'] == "gold": 143 | self.message.emit( 144 | f"** {jd['data']['uname']} {jd['data']['action']}了 {jd['data']['num']} 个 {jd['data']['giftName']}" 145 | ) 146 | elif jd['cmd'] == 'USER_TOAST_MSG': 147 | self.message.emit( 148 | f"** {jd['data']['username']} 上了 {jd['data']['num']} 个 {captainName[jd['data']['guard_level']]}" 149 | ) 150 | elif jd['cmd'] == 'ROOM_BLOCK_MSG': 151 | self.message.emit( 152 | f"** 用户 {jd['data']['uname']} 已被管理员禁言" 153 | ) 154 | elif jd['cmd'] == 'INTERACT_WORD': 155 | self.message.emit( 156 | f"## 用户 {jd['data']['uname']} 进入直播间" 157 | ) 158 | elif jd['cmd'] == 'ENTRY_EFFECT': 159 | self.message.emit( 160 | f"## {jd['data']['copy_writing_v2']}" 161 | ) 162 | elif jd['cmd'] == 'COMBO_SEND': 163 | self.message.emit( 164 | f"** {jd['data']['uname']} 共{jd['data']['action']}了 {jd['data']['combo_num']} 个 {jd['data']['gift_name']}" 165 | ) 166 | except: 167 | logging.exception('弹幕输出失败') 168 | 169 | def setRoomID(self, roomID): 170 | self.roomID = int(roomID) 171 | 172 | def run(self): 173 | asyncio.set_event_loop(asyncio.new_event_loop()) 174 | asyncio.get_event_loop().run_until_complete(self.startup()) 175 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiowebsocket==1.0.0.dev2 2 | certifi==2020.12.5 3 | chardet==4.0.0 4 | dnspython==2.1.0 5 | idna==2.10 6 | PyQt5==5.15.2 7 | PyQt5-sip==12.8.1 8 | python-vlc==3.0.11115 9 | requests==2.25.1 10 | urllib3==1.26.3 11 | pyinstaller==4.2 12 | -------------------------------------------------------------------------------- /scripts/build_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | pyinstaller --clean --noconfirm DDMonitor_unix.spec 3 | cp scripts/run.sh dist/DDMonitor/ -------------------------------------------------------------------------------- /scripts/build_macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | pyinstaller --clean --noconfirm DDMonitor_macos.spec -------------------------------------------------------------------------------- /scripts/build_win.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | RMDIR /S /Q dist 3 | RMDIR /S /Q build 4 | pyinstaller --clean --noconfirm DDMonitor.spec 5 | mkdir dist\DDMonitor\logs 6 | copy utils\config_default.json dist\DDMonitor\utils\config.json 7 | 8 | rem remove useless dll 9 | del /F /Q dist\DDMonitor\Qt5DBus.dll 10 | del /F /Q dist\DDMonitor\Qt5Network.dll 11 | del /F /Q dist\DDMonitor\Qt5Qml.dll 12 | del /F /Q dist\DDMonitor\Qt5QmlModels.dll 13 | del /F /Q dist\DDMonitor\Qt5Quick.dll 14 | del /F /Q dist\DDMonitor\Qt5Svg.dll 15 | del /F /Q dist\DDMonitor\Qt5WebSockets.dll 16 | 17 | del /F /Q dist\DDMonitor\libEGL.dll 18 | del /F /Q dist\DDMonitor\libGLESv2.dll 19 | 20 | del /F /Q dist\DDMonitor\opengl32sw.dll 21 | del /F /Q dist\DDMonitor\ucrtbase.dll 22 | 23 | del /F /Q dist\DDMonitor\pyexpat.pyd 24 | del /F /Q dist\DDMonitor\_decimal.pyd 25 | del /F /Q dist\DDMonitor\_multiprocessing.pyd 26 | 27 | rem Remove dir 28 | RMDIR /S /Q dist\DDMonitor\PyQt5\Qt\translations 29 | RMDIR /S /Q dist\DDMonitor\Include 30 | -------------------------------------------------------------------------------- /scripts/run.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | chcp 65001 rem utf8 3 | cls 4 | mode con: cols=130 lines=40 5 | SET script_path=%~dp0 6 | color 3E 7 | type %script_path%\ascii.txt 8 | rem Banner 9 | set "START_STR=欢迎使用DD监控室^! 本窗口为监控室的调试窗口,请保持打开。监控室约在5秒后运行。" 10 | 11 | echo **************************************************************************************************** 12 | echo %START_STR% 13 | echo **************************************************************************************************** 14 | 15 | rem 5秒间隔 16 | timeout 5 17 | cls 18 | %script_path%\DDMonitor.exe -------------------------------------------------------------------------------- /scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | export VLC_PLUGIN_PATH=/usr/lib/x86_64-linux-gnu/vlc/plugins/ 3 | ./DDMonitor -------------------------------------------------------------------------------- /scripts/sign_macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | AC_OTP_PASS="@keychain:AC_PASSWORD" 3 | PRODUCT_NAME="DDMonitor" 4 | APP_PATH="$PRODUCT_NAME.app" 5 | ZIP_PATH="$PRODUCT_NAME.zip" 6 | codesign --deep --force --verbose \ 7 | --sign $CERT_ID --entitlements ../utils/entitlements.plist \ 8 | -o runtime $APP_PATH 9 | /usr/bin/ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH" 10 | xcrun altool --notarize-app \ 11 | --primary-bundle-id "com.github.zhimingshenjun.ddmonitor" \ 12 | --username $AC_USERNAME --password $AC_OTP_PASS \ 13 | --file $ZIP_PATH -------------------------------------------------------------------------------- /utils/ascii.txt: -------------------------------------------------------------------------------- 1 | ___ ___ 2 | /\ \ /\ \ 3 | /::\ \ /::\ \ 4 | /:/\:\ \ /:/\:\ \ 5 | /:/ \:\__\ /:/ \:\__\ 6 | /:/__/ \:|__| /:/__/ \:|__| 7 | \:\ \ /:/ / \:\ \ /:/ / 8 | \:\ /:/ / \:\ /:/ / 9 | \:\/:/ / \:\/:/ / 10 | \::/__/ \::/__/ 11 | ~~ ~~ 12 | ___ ___ ___ ___ ___ ___ 13 | /\__\ /\ \ /\__\ ___ /\ \ /\ \ /\ \ 14 | /::| | /::\ \ /::| | /\ \ \:\ \ /::\ \ /::\ \ 15 | /:|:| | /:/\:\ \ /:|:| | \:\ \ \:\ \ /:/\:\ \ /:/\:\ \ 16 | /:/|:|__|__ /:/ \:\ \ /:/|:| |__ /::\__\ /::\ \ /:/ \:\ \ /::\~\:\ \ 17 | /:/ |::::\__\ /:/__/ \:\__\ /:/ |:| /\__\ __/:/\/__/ /:/\:\__\ /:/__/ \:\__\ /:/\:\ \:\__\ 18 | \/__/~~/:/ / \:\ \ /:/ / \/__|:|/:/ / /\/:/ / /:/ \/__/ \:\ \ /:/ / \/_|::\/:/ / 19 | /:/ / \:\ /:/ / |:/:/ / \::/__/ /:/ / \:\ /:/ / |:|::/ / 20 | /:/ / \:\/:/ / |::/ / \:\__\ \/__/ \:\/:/ / |:|\/__/ 21 | /:/ / \::/ / /:/ / \/__/ \::/ / |:| | 22 | \/__/ \/__/ \/__/ \/__/ \|__| 23 | -------------------------------------------------------------------------------- /utils/danmu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhimingshenjun/DD_Monitor/0e7fe77a370c69a4dec5aefe29ea99f21760c2b1/utils/danmu.png -------------------------------------------------------------------------------- /utils/entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.disable-executable-page-protection 10 | 11 | com.apple.security.cs.disable-library-validation 12 | 13 | com.apple.security.cs.allow-dyld-environment-variables 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /utils/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | DD监控室帮助 9 | 10 | 11 |
12 |

DD监控室使用帮助

13 |
14 |

运行

15 |
16 |

Windows运行

17 |

解压后运行 run.batDDMonitor.exe 即可。

18 |
19 |
20 |

MacOS上运行

21 |

运行DDMonitor.app即可。

22 |

在首次运行时,需要到系统设置->安全中允许DD监控室运行。

23 |
24 |
25 |

Linux上运行

26 |

确保安装VLC后,运行run.sh即可。

27 |
28 |
29 |
30 |

注意事项

31 |
    32 |
  • 视频缓存默认2G,可以在设置中配置。
  • 33 |
  • 默认启动时加载弹幕,若出现网络稳定性或程序卡死,可以禁用默认加载弹幕。
  • 34 |
35 |
36 |
37 | 38 | -------------------------------------------------------------------------------- /utils/qdark.qss: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License 3 | 4 | Copyright (c) 2013-2018 Colin Duquesnoy https://github.com/ColinDuquesnoy/QDarkStyleSheet/blob/master/LICENSE.md 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to 9 | whom the Software is furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 12 | Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 15 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | */ 20 | 21 | 22 | QToolTip { 23 | border: 1px solid #76797C; 24 | background-color: #5A7566; 25 | color: white; 26 | padding: 0px; /*remove padding, for fix combobox tooltip.*/ 27 | opacity: 255; 28 | } 29 | 30 | QWidget { 31 | color: #eff0f1; 32 | background-color: #31363b; 33 | selection-background-color: #3daee9; 34 | selection-color: #eff0f1; 35 | background-clip: border; 36 | border-image: none; 37 | border: 0px transparent black; 38 | outline: 0; 39 | } 40 | 41 | QWidget:item:hover { 42 | background-color: #18465d; 43 | color: #eff0f1; 44 | } 45 | 46 | QWidget:item:selected { 47 | background-color: #18465d; 48 | } 49 | 50 | QGroupBox::indicator { 51 | width: 18px; 52 | height: 18px; 53 | } 54 | 55 | QGroupBox::indicator { 56 | margin-left: 2px; 57 | } 58 | 59 | QGroupBox::indicator:unchecked { 60 | image: url(:/qss_icons/rc/checkbox_unchecked.png); 61 | } 62 | 63 | QGroupBox::indicator:unchecked:hover, 64 | QGroupBox::indicator:unchecked:focus, 65 | QGroupBox::indicator:unchecked:pressed { 66 | border: none; 67 | image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); 68 | } 69 | 70 | QGroupBox::indicator:checked { 71 | image: url(:/qss_icons/rc/checkbox_checked.png); 72 | } 73 | 74 | QGroupBox::indicator:checked:hover, 75 | QGroupBox::indicator:checked:focus, 76 | QGroupBox::indicator:checked:pressed { 77 | border: none; 78 | image: url(:/qss_icons/rc/checkbox_checked_focus.png); 79 | } 80 | 81 | QCheckBox::indicator:indeterminate { 82 | image: url(:/qss_icons/rc/checkbox_indeterminate.png); 83 | } 84 | 85 | QCheckBox::indicator:indeterminate:focus, 86 | QCheckBox::indicator:indeterminate:hover, 87 | QCheckBox::indicator:indeterminate:pressed { 88 | image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); 89 | } 90 | 91 | QCheckBox::indicator:checked:disabled, 92 | QGroupBox::indicator:checked:disabled { 93 | image: url(:/qss_icons/rc/checkbox_checked_disabled.png); 94 | } 95 | 96 | QCheckBox::indicator:unchecked:disabled, 97 | QGroupBox::indicator:unchecked:disabled { 98 | image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); 99 | } 100 | 101 | QRadioButton { 102 | spacing: 5px; 103 | outline: none; 104 | color: #eff0f1; 105 | margin-bottom: 2px; 106 | } 107 | 108 | QRadioButton:disabled { 109 | color: #76797C; 110 | } 111 | 112 | QRadioButton::indicator { 113 | width: 21px; 114 | height: 21px; 115 | } 116 | 117 | QRadioButton::indicator:unchecked { 118 | image: url(:/qss_icons/rc/radio_unchecked.png); 119 | } 120 | 121 | QRadioButton::indicator:unchecked:hover, 122 | QRadioButton::indicator:unchecked:focus, 123 | QRadioButton::indicator:unchecked:pressed { 124 | border: none; 125 | outline: none; 126 | image: url(:/qss_icons/rc/radio_unchecked_focus.png); 127 | } 128 | 129 | QRadioButton::indicator:checked { 130 | border: none; 131 | outline: none; 132 | image: url(:/qss_icons/rc/radio_checked.png); 133 | } 134 | 135 | QRadioButton::indicator:checked:hover, 136 | QRadioButton::indicator:checked:focus, 137 | QRadioButton::indicator:checked:pressed { 138 | border: none; 139 | outline: none; 140 | image: url(:/qss_icons/rc/radio_checked_focus.png); 141 | } 142 | 143 | QRadioButton::indicator:checked:disabled { 144 | outline: none; 145 | image: url(:/qss_icons/rc/radio_checked_disabled.png); 146 | } 147 | 148 | QRadioButton::indicator:unchecked:disabled { 149 | image: url(:/qss_icons/rc/radio_unchecked_disabled.png); 150 | } 151 | 152 | QMenuBar { 153 | background-color: #31363b; 154 | color: #eff0f1; 155 | } 156 | 157 | QMenuBar::item { 158 | background: transparent; 159 | } 160 | 161 | QMenuBar::item:selected { 162 | background: transparent; 163 | border: 1px solid #76797C; 164 | } 165 | 166 | QMenuBar::item:pressed { 167 | border: 1px solid #76797C; 168 | background-color: #3daee9; 169 | color: #eff0f1; 170 | margin-bottom: -1px; 171 | padding-bottom: 1px; 172 | } 173 | 174 | QMenu { 175 | border: 1px solid #76797C; 176 | color: #eff0f1; 177 | margin: 2px; 178 | } 179 | 180 | QMenu::icon { 181 | margin: 5px; 182 | } 183 | 184 | QMenu::item { 185 | padding: 5px 30px 5px 30px; 186 | border: 1px solid transparent; 187 | /* reserve space for selection border */ 188 | } 189 | 190 | QMenu::item:selected { 191 | color: #eff0f1; 192 | } 193 | 194 | QMenu::separator { 195 | height: 2px; 196 | background: lightblue; 197 | margin-left: 10px; 198 | margin-right: 5px; 199 | } 200 | 201 | QMenu::indicator { 202 | width: 18px; 203 | height: 18px; 204 | } 205 | 206 | 207 | /* non-exclusive indicator = check box style indicator 208 | (see QActionGroup::setExclusive) */ 209 | 210 | QMenu::indicator:non-exclusive:unchecked { 211 | image: url(:/qss_icons/rc/checkbox_unchecked.png); 212 | } 213 | 214 | QMenu::indicator:non-exclusive:unchecked:selected { 215 | image: url(:/qss_icons/rc/checkbox_unchecked_disabled.png); 216 | } 217 | 218 | QMenu::indicator:non-exclusive:checked { 219 | image: url(:/qss_icons/rc/checkbox_checked.png); 220 | } 221 | 222 | QMenu::indicator:non-exclusive:checked:selected { 223 | image: url(:/qss_icons/rc/checkbox_checked_disabled.png); 224 | } 225 | 226 | 227 | /* exclusive indicator = radio button style indicator (see QActionGroup::setExclusive) */ 228 | 229 | QMenu::indicator:exclusive:unchecked { 230 | image: url(:/qss_icons/rc/radio_unchecked.png); 231 | } 232 | 233 | QMenu::indicator:exclusive:unchecked:selected { 234 | image: url(:/qss_icons/rc/radio_unchecked_disabled.png); 235 | } 236 | 237 | QMenu::indicator:exclusive:checked { 238 | image: url(:/qss_icons/rc/radio_checked.png); 239 | } 240 | 241 | QMenu::indicator:exclusive:checked:selected { 242 | image: url(:/qss_icons/rc/radio_checked_disabled.png); 243 | } 244 | 245 | QMenu::right-arrow { 246 | margin: 5px; 247 | image: url(:/qss_icons/rc/right_arrow.png) 248 | } 249 | 250 | QWidget:disabled { 251 | color: #454545; 252 | background-color: #31363b; 253 | } 254 | 255 | QAbstractItemView { 256 | alternate-background-color: #31363b; 257 | color: #eff0f1; 258 | border: 1px solid #3A3939; 259 | border-radius: 2px; 260 | } 261 | 262 | QWidget:focus, 263 | QMenuBar:focus { 264 | border: 1px solid #3daee9; 265 | } 266 | 267 | QTabWidget:focus, 268 | QCheckBox:focus, 269 | QRadioButton:focus, 270 | QSlider:focus { 271 | border: none; 272 | } 273 | 274 | QLineEdit { 275 | background-color: #232629; 276 | padding: 5px; 277 | border-style: solid; 278 | border: 1px solid #76797C; 279 | border-radius: 2px; 280 | color: #eff0f1; 281 | } 282 | 283 | QAbstractItemView QLineEdit { 284 | padding: 0; 285 | } 286 | 287 | QGroupBox { 288 | border: 1px solid #76797C; 289 | border-radius: 2px; 290 | margin-top: 20px; 291 | } 292 | 293 | QGroupBox::title { 294 | subcontrol-origin: margin; 295 | subcontrol-position: top center; 296 | padding-left: 10px; 297 | padding-right: 10px; 298 | padding-top: 10px; 299 | } 300 | 301 | QAbstractScrollArea { 302 | border-radius: 2px; 303 | border: 1px solid #76797C; 304 | background-color: transparent; 305 | } 306 | 307 | QScrollBar:horizontal { 308 | height: 15px; 309 | margin: 3px 15px 3px 15px; 310 | border: 1px transparent #2A2929; 311 | border-radius: 4px; 312 | background-color: #2A2929; 313 | } 314 | 315 | QScrollBar::handle:horizontal { 316 | background-color: #605F5F; 317 | min-width: 5px; 318 | border-radius: 4px; 319 | } 320 | 321 | QScrollBar::add-line:horizontal { 322 | margin: 0px 3px 0px 3px; 323 | border-image: url(:/qss_icons/rc/right_arrow_disabled.png); 324 | width: 10px; 325 | height: 10px; 326 | subcontrol-position: right; 327 | subcontrol-origin: margin; 328 | } 329 | 330 | QScrollBar::sub-line:horizontal { 331 | margin: 0px 3px 0px 3px; 332 | border-image: url(:/qss_icons/rc/left_arrow_disabled.png); 333 | height: 10px; 334 | width: 10px; 335 | subcontrol-position: left; 336 | subcontrol-origin: margin; 337 | } 338 | 339 | QScrollBar::add-line:horizontal:hover, 340 | QScrollBar::add-line:horizontal:on { 341 | border-image: url(:/qss_icons/rc/right_arrow.png); 342 | height: 10px; 343 | width: 10px; 344 | subcontrol-position: right; 345 | subcontrol-origin: margin; 346 | } 347 | 348 | QScrollBar::sub-line:horizontal:hover, 349 | QScrollBar::sub-line:horizontal:on { 350 | border-image: url(:/qss_icons/rc/left_arrow.png); 351 | height: 10px; 352 | width: 10px; 353 | subcontrol-position: left; 354 | subcontrol-origin: margin; 355 | } 356 | 357 | QScrollBar::up-arrow:horizontal, 358 | QScrollBar::down-arrow:horizontal { 359 | background: none; 360 | } 361 | 362 | QScrollBar::add-page:horizontal, 363 | QScrollBar::sub-page:horizontal { 364 | background: none; 365 | } 366 | 367 | QScrollBar:vertical { 368 | background-color: #2A2929; 369 | width: 15px; 370 | margin: 15px 3px 15px 3px; 371 | border: 1px transparent #2A2929; 372 | border-radius: 4px; 373 | } 374 | 375 | QScrollBar::handle:vertical { 376 | background-color: #605F5F; 377 | min-height: 40px; 378 | border-radius: 4px; 379 | } 380 | 381 | QScrollBar::sub-line:vertical { 382 | margin: 3px 0px 3px 0px; 383 | border-image: url(:/qss_icons/rc/up_arrow_disabled.png); 384 | height: 10px; 385 | width: 10px; 386 | subcontrol-position: top; 387 | subcontrol-origin: margin; 388 | } 389 | 390 | QScrollBar::add-line:vertical { 391 | margin: 3px 0px 3px 0px; 392 | border-image: url(:/qss_icons/rc/down_arrow_disabled.png); 393 | height: 10px; 394 | width: 10px; 395 | subcontrol-position: bottom; 396 | subcontrol-origin: margin; 397 | } 398 | 399 | QScrollBar::sub-line:vertical:hover, 400 | QScrollBar::sub-line:vertical:on { 401 | border-image: url(:/qss_icons/rc/up_arrow.png); 402 | height: 10px; 403 | width: 10px; 404 | subcontrol-position: top; 405 | subcontrol-origin: margin; 406 | } 407 | 408 | QScrollBar::add-line:vertical:hover, 409 | QScrollBar::add-line:vertical:on { 410 | border-image: url(:/qss_icons/rc/down_arrow.png); 411 | height: 10px; 412 | width: 10px; 413 | subcontrol-position: bottom; 414 | subcontrol-origin: margin; 415 | } 416 | 417 | QScrollBar::up-arrow:vertical, 418 | QScrollBar::down-arrow:vertical { 419 | background: none; 420 | } 421 | 422 | QScrollBar::add-page:vertical, 423 | QScrollBar::sub-page:vertical { 424 | background: none; 425 | } 426 | 427 | QTextEdit { 428 | background-color: #232629; 429 | color: #eff0f1; 430 | border: 1px solid #76797C; 431 | } 432 | 433 | QPlainTextEdit { 434 | background-color: #232629; 435 | ; 436 | color: #eff0f1; 437 | border-radius: 2px; 438 | border: 1px solid #76797C; 439 | } 440 | 441 | QHeaderView::section { 442 | background-color: #76797C; 443 | color: #eff0f1; 444 | padding: 5px; 445 | border: 1px solid #76797C; 446 | } 447 | 448 | QSizeGrip { 449 | image: url(:/qss_icons/rc/sizegrip.png); 450 | width: 12px; 451 | height: 12px; 452 | } 453 | 454 | QMainWindow::separator { 455 | background-color: #31363b; 456 | color: white; 457 | padding-left: 4px; 458 | spacing: 2px; 459 | border: 1px dashed #76797C; 460 | } 461 | 462 | QMainWindow::separator:hover { 463 | background-color: #787876; 464 | color: white; 465 | padding-left: 4px; 466 | border: 1px solid #76797C; 467 | spacing: 2px; 468 | } 469 | 470 | QMenu::separator { 471 | height: 1px; 472 | background-color: #76797C; 473 | color: white; 474 | padding-left: 4px; 475 | margin-left: 10px; 476 | margin-right: 5px; 477 | } 478 | 479 | QFrame { 480 | border-radius: 2px; 481 | border: 1px solid #76797C; 482 | } 483 | 484 | QFrame[frameShape="0"] { 485 | border-radius: 2px; 486 | border: 1px transparent #76797C; 487 | } 488 | 489 | QStackedWidget { 490 | border: 1px transparent black; 491 | } 492 | 493 | QToolBar { 494 | border: 1px transparent #393838; 495 | background: 1px solid #31363b; 496 | font-weight: bold; 497 | } 498 | 499 | QToolBar::handle:horizontal { 500 | image: url(:/qss_icons/rc/Hmovetoolbar.png); 501 | } 502 | 503 | QToolBar::handle:vertical { 504 | image: url(:/qss_icons/rc/Vmovetoolbar.png); 505 | } 506 | 507 | QToolBar::separator:horizontal { 508 | image: url(:/qss_icons/rc/Hsepartoolbar.png); 509 | } 510 | 511 | QToolBar::separator:vertical { 512 | image: url(:/qss_icons/rc/Vsepartoolbar.png); 513 | } 514 | 515 | QToolButton#qt_toolbar_ext_button { 516 | background: #58595a 517 | } 518 | 519 | QPushButton { 520 | color: #eff0f1; 521 | background-color: #31363b; 522 | border-width: 0px; 523 | border-color: #76797C; 524 | border-style: solid; 525 | padding: 5px; 526 | border-radius: 2px; 527 | outline: none; 528 | } 529 | 530 | QPushButton:disabled { 531 | background-color: #31363b; 532 | border-width: 0px; 533 | border-color: #454545; 534 | border-style: solid; 535 | padding-top: 5px; 536 | padding-bottom: 5px; 537 | padding-left: 10px; 538 | padding-right: 10px; 539 | border-radius: 2px; 540 | color: #454545; 541 | } 542 | 543 | QPushButton:pressed { 544 | background-color: #3daee9; 545 | padding-top: -15px; 546 | padding-bottom: -17px; 547 | } 548 | 549 | QComboBox { 550 | selection-background-color: #3daee9; 551 | border-style: solid; 552 | border: 1px solid #76797C; 553 | border-radius: 2px; 554 | padding: 5px; 555 | } 556 | 557 | QPushButton:checked { 558 | background-color: #76797C; 559 | border-color: #6A6969; 560 | } 561 | 562 | QComboBox:hover, 563 | QPushButton:hover, 564 | QAbstractSpinBox:hover, 565 | QLineEdit:hover, 566 | QTextEdit:hover, 567 | QPlainTextEdit:hover, 568 | QAbstractView:hover, 569 | QTreeView:hover { 570 | border: 1px solid #3daee9; 571 | color: #eff0f1; 572 | } 573 | 574 | QComboBox:on { 575 | padding-top: 3px; 576 | padding-left: 4px; 577 | selection-background-color: #4a4a4a; 578 | } 579 | 580 | QComboBox QAbstractItemView { 581 | background-color: #232629; 582 | border-radius: 2px; 583 | border: 1px solid #76797C; 584 | selection-background-color: #18465d; 585 | } 586 | 587 | QComboBox::drop-down { 588 | subcontrol-origin: padding; 589 | subcontrol-position: top right; 590 | width: 15px; 591 | border-left-width: 0px; 592 | border-left-color: darkgray; 593 | border-left-style: solid; 594 | border-top-right-radius: 3px; 595 | border-bottom-right-radius: 3px; 596 | } 597 | 598 | QComboBox::down-arrow { 599 | image: url(:/qss_icons/rc/down_arrow_disabled.png); 600 | } 601 | 602 | QComboBox::down-arrow:on, 603 | QComboBox::down-arrow:hover, 604 | QComboBox::down-arrow:focus { 605 | image: url(:/qss_icons/rc/down_arrow.png); 606 | } 607 | 608 | QAbstractSpinBox { 609 | padding: 5px; 610 | border: 1px solid #76797C; 611 | background-color: #232629; 612 | color: #eff0f1; 613 | border-radius: 2px; 614 | min-width: 75px; 615 | } 616 | 617 | QAbstractSpinBox:up-button { 618 | background-color: transparent; 619 | subcontrol-origin: border; 620 | subcontrol-position: center right; 621 | } 622 | 623 | QAbstractSpinBox:down-button { 624 | background-color: transparent; 625 | subcontrol-origin: border; 626 | subcontrol-position: center left; 627 | } 628 | 629 | QAbstractSpinBox::up-arrow, 630 | QAbstractSpinBox::up-arrow:disabled, 631 | QAbstractSpinBox::up-arrow:off { 632 | image: url(:/qss_icons/rc/up_arrow_disabled.png); 633 | width: 10px; 634 | height: 10px; 635 | } 636 | 637 | QAbstractSpinBox::up-arrow:hover { 638 | image: url(:/qss_icons/rc/up_arrow.png); 639 | } 640 | 641 | QAbstractSpinBox::down-arrow, 642 | QAbstractSpinBox::down-arrow:disabled, 643 | QAbstractSpinBox::down-arrow:off { 644 | image: url(:/qss_icons/rc/down_arrow_disabled.png); 645 | width: 10px; 646 | height: 10px; 647 | } 648 | 649 | QAbstractSpinBox::down-arrow:hover { 650 | image: url(:/qss_icons/rc/down_arrow.png); 651 | } 652 | 653 | QLabel { 654 | border: 0px solid black; 655 | } 656 | 657 | QTabWidget { 658 | border: 0px transparent black; 659 | } 660 | 661 | QTabWidget::pane { 662 | border: 1px solid #76797C; 663 | padding: 5px; 664 | margin: 0px; 665 | } 666 | 667 | QTabWidget::tab-bar { 668 | /* left: 5px; move to the right by 5px */ 669 | } 670 | 671 | QTabBar { 672 | qproperty-drawBase: 0; 673 | border-radius: 3px; 674 | } 675 | 676 | QTabBar:focus { 677 | border: 0px transparent black; 678 | } 679 | 680 | QTabBar::close-button { 681 | image: url(:/qss_icons/rc/close.png); 682 | background: transparent; 683 | } 684 | 685 | QTabBar::close-button:hover { 686 | image: url(:/qss_icons/rc/close-hover.png); 687 | background: transparent; 688 | } 689 | 690 | QTabBar::close-button:pressed { 691 | image: url(:/qss_icons/rc/close-pressed.png); 692 | background: transparent; 693 | } 694 | 695 | 696 | /* TOP TABS */ 697 | 698 | QTabBar::tab:top { 699 | color: #eff0f1; 700 | border: 1px solid #76797C; 701 | border-bottom: 1px transparent black; 702 | background-color: #31363b; 703 | padding: 5px; 704 | min-width: 50px; 705 | border-top-left-radius: 2px; 706 | border-top-right-radius: 2px; 707 | } 708 | 709 | QTabBar::tab:top:selected { 710 | color: #eff0f1; 711 | background-color: #54575B; 712 | border: 1px solid #76797C; 713 | border-bottom: 2px solid #3daee9; 714 | border-top-left-radius: 2px; 715 | border-top-right-radius: 2px; 716 | } 717 | 718 | QTabBar::tab:top:!selected:hover { 719 | background-color: #3daee9; 720 | } 721 | 722 | 723 | /* BOTTOM TABS */ 724 | 725 | QTabBar::tab:bottom { 726 | color: #eff0f1; 727 | border: 1px solid #76797C; 728 | border-top: 1px transparent black; 729 | background-color: #31363b; 730 | padding: 5px; 731 | border-bottom-left-radius: 2px; 732 | border-bottom-right-radius: 2px; 733 | min-width: 50px; 734 | } 735 | 736 | QTabBar::tab:bottom:selected { 737 | color: #eff0f1; 738 | background-color: #54575B; 739 | border: 1px solid #76797C; 740 | border-top: 2px solid #3daee9; 741 | border-bottom-left-radius: 2px; 742 | border-bottom-right-radius: 2px; 743 | } 744 | 745 | QTabBar::tab:bottom:!selected:hover { 746 | background-color: #3daee9; 747 | } 748 | 749 | 750 | /* LEFT TABS */ 751 | 752 | QTabBar::tab:left { 753 | color: #eff0f1; 754 | border: 1px solid #76797C; 755 | border-left: 1px transparent black; 756 | background-color: #31363b; 757 | padding: 5px; 758 | border-top-right-radius: 2px; 759 | border-bottom-right-radius: 2px; 760 | min-height: 50px; 761 | } 762 | 763 | QTabBar::tab:left:selected { 764 | color: #eff0f1; 765 | background-color: #54575B; 766 | border: 1px solid #76797C; 767 | border-left: 2px solid #3daee9; 768 | border-top-right-radius: 2px; 769 | border-bottom-right-radius: 2px; 770 | } 771 | 772 | QTabBar::tab:left:!selected:hover { 773 | background-color: #3daee9; 774 | } 775 | 776 | 777 | /* RIGHT TABS */ 778 | 779 | QTabBar::tab:right { 780 | color: #eff0f1; 781 | border: 1px solid #76797C; 782 | border-right: 1px transparent black; 783 | background-color: #31363b; 784 | padding: 5px; 785 | border-top-left-radius: 2px; 786 | border-bottom-left-radius: 2px; 787 | min-height: 50px; 788 | } 789 | 790 | QTabBar::tab:right:selected { 791 | color: #eff0f1; 792 | background-color: #54575B; 793 | border: 1px solid #76797C; 794 | border-right: 2px solid #3daee9; 795 | border-top-left-radius: 2px; 796 | border-bottom-left-radius: 2px; 797 | } 798 | 799 | QTabBar::tab:right:!selected:hover { 800 | background-color: #3daee9; 801 | } 802 | 803 | QTabBar QToolButton::right-arrow:enabled { 804 | image: url(:/qss_icons/rc/right_arrow.png); 805 | } 806 | 807 | QTabBar QToolButton::left-arrow:enabled { 808 | image: url(:/qss_icons/rc/left_arrow.png); 809 | } 810 | 811 | QTabBar QToolButton::right-arrow:disabled { 812 | image: url(:/qss_icons/rc/right_arrow_disabled.png); 813 | } 814 | 815 | QTabBar QToolButton::left-arrow:disabled { 816 | image: url(:/qss_icons/rc/left_arrow_disabled.png); 817 | } 818 | 819 | QDockWidget { 820 | background: #31363b; 821 | border: 1px solid #403F3F; 822 | titlebar-close-icon: url(:/qss_icons/rc/close.png); 823 | titlebar-normal-icon: url(:/qss_icons/rc/undock.png); 824 | } 825 | 826 | QDockWidget::close-button, 827 | QDockWidget::float-button { 828 | border: 1px solid transparent; 829 | border-radius: 2px; 830 | background: transparent; 831 | } 832 | 833 | QDockWidget::close-button:hover, 834 | QDockWidget::float-button:hover { 835 | background: rgba(255, 255, 255, 10); 836 | } 837 | 838 | QDockWidget::close-button:pressed, 839 | QDockWidget::float-button:pressed { 840 | padding: 1px -1px -1px 1px; 841 | background: rgba(255, 255, 255, 10); 842 | } 843 | 844 | QTreeView, 845 | QListView { 846 | border: 1px solid #76797C; 847 | background-color: #232629; 848 | } 849 | 850 | QTreeView:branch:selected, 851 | QTreeView:branch:hover { 852 | background: url(:/qss_icons/rc/transparent.png); 853 | } 854 | 855 | QTreeView::branch:has-siblings:!adjoins-item { 856 | border-image: url(:/qss_icons/rc/transparent.png); 857 | } 858 | 859 | QTreeView::branch:has-siblings:adjoins-item { 860 | border-image: url(:/qss_icons/rc/transparent.png); 861 | } 862 | 863 | QTreeView::branch:!has-children:!has-siblings:adjoins-item { 864 | border-image: url(:/qss_icons/rc/transparent.png); 865 | } 866 | 867 | QTreeView::branch:has-children:!has-siblings:closed, 868 | QTreeView::branch:closed:has-children:has-siblings { 869 | image: url(:/qss_icons/rc/branch_closed.png); 870 | } 871 | 872 | QTreeView::branch:open:has-children:!has-siblings, 873 | QTreeView::branch:open:has-children:has-siblings { 874 | image: url(:/qss_icons/rc/branch_open.png); 875 | } 876 | 877 | QTreeView::branch:has-children:!has-siblings:closed:hover, 878 | QTreeView::branch:closed:has-children:has-siblings:hover { 879 | image: url(:/qss_icons/rc/branch_closed-on.png); 880 | } 881 | 882 | QTreeView::branch:open:has-children:!has-siblings:hover, 883 | QTreeView::branch:open:has-children:has-siblings:hover { 884 | image: url(:/qss_icons/rc/branch_open-on.png); 885 | } 886 | 887 | QListView::item:!selected:hover, 888 | QTreeView::item:!selected:hover { 889 | background: #18465d; 890 | outline: 0; 891 | color: #eff0f1 892 | } 893 | 894 | QListView::item:selected:hover, 895 | QTreeView::item:selected:hover { 896 | background: #287399; 897 | color: #eff0f1; 898 | } 899 | 900 | QTreeView::indicator:checked, 901 | QListView::indicator:checked { 902 | image: url(:/qss_icons/rc/checkbox_checked.png); 903 | } 904 | 905 | QTreeView::indicator:unchecked, 906 | QListView::indicator:unchecked { 907 | image: url(:/qss_icons/rc/checkbox_unchecked.png); 908 | } 909 | 910 | QTreeView::indicator:indeterminate, 911 | QListView::indicator:indeterminate { 912 | image: url(:/qss_icons/rc/checkbox_indeterminate.png); 913 | } 914 | 915 | QTreeView::indicator:checked:hover, 916 | QTreeView::indicator:checked:focus, 917 | QTreeView::indicator:checked:pressed, 918 | QListView::indicator:checked:hover, 919 | QListView::indicator:checked:focus, 920 | QListView::indicator:checked:pressed { 921 | image: url(:/qss_icons/rc/checkbox_checked_focus.png); 922 | } 923 | 924 | QTreeView::indicator:unchecked:hover, 925 | QTreeView::indicator:unchecked:focus, 926 | QTreeView::indicator:unchecked:pressed, 927 | QListView::indicator:unchecked:hover, 928 | QListView::indicator:unchecked:focus, 929 | QListView::indicator:unchecked:pressed { 930 | image: url(:/qss_icons/rc/checkbox_unchecked_focus.png); 931 | } 932 | 933 | QTreeView::indicator:indeterminate:hover, 934 | QTreeView::indicator:indeterminate:focus, 935 | QTreeView::indicator:indeterminate:pressed, 936 | QListView::indicator:indeterminate:hover, 937 | QListView::indicator:indeterminate:focus, 938 | QListView::indicator:indeterminate:pressed { 939 | image: url(:/qss_icons/rc/checkbox_indeterminate_focus.png); 940 | } 941 | 942 | QSlider::groove:horizontal { 943 | border: 1px solid #565a5e; 944 | height: 4px; 945 | background: #565a5e; 946 | margin: 0px; 947 | border-radius: 2px; 948 | } 949 | 950 | QSlider::handle:horizontal { 951 | background: #232629; 952 | border: 1px solid #565a5e; 953 | width: 16px; 954 | height: 16px; 955 | margin: -8px 0; 956 | border-radius: 9px; 957 | } 958 | 959 | QSlider::groove:vertical { 960 | border: 1px solid #565a5e; 961 | width: 4px; 962 | background: #565a5e; 963 | margin: 0px; 964 | border-radius: 3px; 965 | } 966 | 967 | QSlider::handle:vertical { 968 | background: #232629; 969 | border: 1px solid #565a5e; 970 | width: 16px; 971 | height: 16px; 972 | margin: 0 -8px; 973 | border-radius: 9px; 974 | } 975 | 976 | QToolButton { 977 | background-color: transparent; 978 | border: 1px transparent #76797C; 979 | border-radius: 2px; 980 | margin: 3px; 981 | padding: 5px; 982 | } 983 | 984 | QToolButton[popupMode="1"] { 985 | /* only for MenuButtonPopup */ 986 | padding-right: 20px; 987 | /* make way for the popup button */ 988 | border: 1px #76797C; 989 | border-radius: 5px; 990 | } 991 | 992 | QToolButton[popupMode="2"] { 993 | /* only for InstantPopup */ 994 | padding-right: 10px; 995 | /* make way for the popup button */ 996 | border: 1px #76797C; 997 | } 998 | 999 | QToolButton:hover, 1000 | QToolButton::menu-button:hover { 1001 | background-color: transparent; 1002 | border: 1px solid #3daee9; 1003 | padding: 5px; 1004 | } 1005 | 1006 | QToolButton:checked, 1007 | QToolButton:pressed, 1008 | QToolButton::menu-button:pressed { 1009 | background-color: #3daee9; 1010 | border: 1px solid #3daee9; 1011 | padding: 5px; 1012 | } 1013 | 1014 | 1015 | /* the subcontrol below is used only in the InstantPopup or DelayedPopup mode */ 1016 | 1017 | QToolButton::menu-indicator { 1018 | image: url(:/qss_icons/rc/down_arrow.png); 1019 | top: -7px; 1020 | left: -2px; 1021 | /* shift it a bit */ 1022 | } 1023 | 1024 | 1025 | /* the subcontrols below are used only in the MenuButtonPopup mode */ 1026 | 1027 | QToolButton::menu-button { 1028 | border: 1px transparent #76797C; 1029 | border-top-right-radius: 6px; 1030 | border-bottom-right-radius: 6px; 1031 | /* 16px width + 4px for border = 20px allocated above */ 1032 | width: 16px; 1033 | outline: none; 1034 | } 1035 | 1036 | QToolButton::menu-arrow { 1037 | image: url(:/qss_icons/rc/down_arrow.png); 1038 | } 1039 | 1040 | QToolButton::menu-arrow:open { 1041 | border: 1px solid #76797C; 1042 | } 1043 | 1044 | QPushButton::menu-indicator { 1045 | subcontrol-origin: padding; 1046 | subcontrol-position: bottom right; 1047 | left: 8px; 1048 | } 1049 | 1050 | QTableView { 1051 | border: 1px solid #76797C; 1052 | gridline-color: #383f46; 1053 | background-color: #232629; 1054 | } 1055 | 1056 | QTableView, 1057 | QHeaderView { 1058 | border-radius: 0px; 1059 | } 1060 | 1061 | QTableView::item:pressed, 1062 | QListView::item:pressed, 1063 | QTreeView::item:pressed { 1064 | background: #18465d; 1065 | color: #eff0f1; 1066 | } 1067 | 1068 | QTableView::item:selected:active, 1069 | QTreeView::item:selected:active, 1070 | QListView::item:selected:active { 1071 | background: #287399; 1072 | color: #eff0f1; 1073 | } 1074 | 1075 | QHeaderView { 1076 | background-color: #31363b; 1077 | border: 1px transparent; 1078 | border-radius: 0px; 1079 | margin: 0px; 1080 | padding: 0px; 1081 | } 1082 | 1083 | QHeaderView::section { 1084 | background-color: #31363b; 1085 | color: #eff0f1; 1086 | padding: 5px; 1087 | border: 1px solid #76797C; 1088 | border-radius: 0px; 1089 | text-align: center; 1090 | } 1091 | 1092 | QHeaderView::section::vertical::first, 1093 | QHeaderView::section::vertical::only-one { 1094 | border-top: 1px solid #76797C; 1095 | } 1096 | 1097 | QHeaderView::section::vertical { 1098 | border-top: transparent; 1099 | } 1100 | 1101 | QHeaderView::section::horizontal::first, 1102 | QHeaderView::section::horizontal::only-one { 1103 | border-left: 1px solid #76797C; 1104 | } 1105 | 1106 | QHeaderView::section::horizontal { 1107 | border-left: transparent; 1108 | } 1109 | 1110 | QHeaderView::section:checked { 1111 | color: white; 1112 | background-color: #334e5e; 1113 | } 1114 | 1115 | 1116 | /* style the sort indicator */ 1117 | 1118 | QHeaderView::down-arrow { 1119 | image: url(:/qss_icons/rc/down_arrow.png); 1120 | } 1121 | 1122 | QHeaderView::up-arrow { 1123 | image: url(:/qss_icons/rc/up_arrow.png); 1124 | } 1125 | 1126 | QTableCornerButton::section { 1127 | background-color: #31363b; 1128 | border: 1px transparent #76797C; 1129 | border-radius: 0px; 1130 | } 1131 | 1132 | QToolBox { 1133 | padding: 5px; 1134 | border: 1px transparent black; 1135 | } 1136 | 1137 | QToolBox::tab { 1138 | color: #eff0f1; 1139 | background-color: #31363b; 1140 | border: 1px solid #76797C; 1141 | border-bottom: 1px transparent #31363b; 1142 | border-top-left-radius: 5px; 1143 | border-top-right-radius: 5px; 1144 | } 1145 | 1146 | QToolBox::tab:selected { 1147 | /* italicize selected tabs */ 1148 | font: italic; 1149 | background-color: #31363b; 1150 | border-color: #3daee9; 1151 | } 1152 | 1153 | QStatusBar::item { 1154 | border: 0px transparent dark; 1155 | } 1156 | 1157 | QFrame[height="3"], 1158 | QFrame[width="3"] { 1159 | background-color: #76797C; 1160 | } 1161 | 1162 | QSplitter::handle { 1163 | border: 1px dashed #76797C; 1164 | } 1165 | 1166 | QSplitter::handle:hover { 1167 | background-color: #787876; 1168 | border: 1px solid #76797C; 1169 | } 1170 | 1171 | QSplitter::handle:horizontal { 1172 | width: 1px; 1173 | } 1174 | 1175 | QSplitter::handle:vertical { 1176 | height: 1px; 1177 | } 1178 | 1179 | QProgressBar { 1180 | border: 1px solid #76797C; 1181 | border-radius: 5px; 1182 | text-align: center; 1183 | } 1184 | 1185 | QProgressBar::chunk { 1186 | background-color: #05B8CC; 1187 | } 1188 | 1189 | QDateEdit { 1190 | selection-background-color: #3daee9; 1191 | border-style: solid; 1192 | border: 1px solid #3375A3; 1193 | border-radius: 2px; 1194 | padding: 1px; 1195 | min-width: 75px; 1196 | } 1197 | 1198 | QDateEdit:on { 1199 | padding-top: 3px; 1200 | padding-left: 4px; 1201 | selection-background-color: #4a4a4a; 1202 | } 1203 | 1204 | QDateEdit QAbstractItemView { 1205 | background-color: #232629; 1206 | border-radius: 2px; 1207 | border: 1px solid #3375A3; 1208 | selection-background-color: #3daee9; 1209 | } 1210 | 1211 | QDateEdit::drop-down { 1212 | subcontrol-origin: padding; 1213 | subcontrol-position: top right; 1214 | width: 15px; 1215 | border-left-width: 0px; 1216 | border-left-color: darkgray; 1217 | border-left-style: solid; 1218 | border-top-right-radius: 3px; 1219 | border-bottom-right-radius: 3px; 1220 | } 1221 | 1222 | QDateEdit::down-arrow { 1223 | image: url(:/qss_icons/rc/down_arrow_disabled.png); 1224 | } 1225 | 1226 | QDateEdit::down-arrow:on, 1227 | QDateEdit::down-arrow:hover, 1228 | QDateEdit::down-arrow:focus { 1229 | image: url(:/qss_icons/rc/down_arrow.png); 1230 | } -------------------------------------------------------------------------------- /utils/splash.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhimingshenjun/DD_Monitor/0e7fe77a370c69a4dec5aefe29ea99f21760c2b1/utils/splash.jpg -------------------------------------------------------------------------------- /utils/vtb.csv: -------------------------------------------------------------------------------- 1 | 神楽七奈Official,21304638,个人势 2 | 咩栗,8792912,个人势 3 | HiiroVTuber,21919321,个人势 4 | 雫るる_Official,21013446,个人势 5 | 星宮汐Official,22047448,个人势 6 | 神楽Mea_Official,12235923,个人势 7 | 夏诺雅_shanoa,1321846,个人势 8 | 呜米,22384516,个人势 9 | 美波七海-official,22571958,个人势 10 | 泰蕾莎Channel,870004,个人势 11 | 凤玲天天Official,22347045,个人势 12 | NoWorld_Official,21448649,个人势 13 | 阿梓从小就很可爱,510,个人势 14 | 阳向心美Official,566227,个人势 15 | 三日暦Official,22205953,个人势 16 | 召唤师yami,7595278,个人势 17 | 早濑弥生Official,22159299,个人势 18 | 新科娘Official,21602686,个人势 19 | 铃音_Official,4316215,个人势 20 | 琴吹夢_official,21799389,个人势 21 | 一包薯条嘻嘻,22603245,个人势 22 | 多多poi丶,392523,个人势 23 | 本气黑猫,15536,个人势 24 | 还有醒着的么,318,个人势 25 | shourei小N,363889,个人势 26 | 星野饼美,694,个人势 27 | 扶桑大红花丶,947866,个人势 28 | AnKe-Poi,305,个人势 29 | v猫诺v,308543,个人势 30 | 小狼XF,846315,个人势 31 | 林簌SUSU,227422,个人势 32 | 战术级子轩,13355,个人势 33 | 闪光Pika_Official,22485731,个人势 34 | 内德维德,5424,个人势 35 | 乌拉の帝国Official,1138,个人势 36 | 花花Haya,7688602,个人势 37 | 佐仓•M•沙耶加,104,个人势 38 | 酥幼妹妹わ,145277,个人势 39 | 一块电鹿板,21727410,个人势 40 | 妙妙子Official,22495291,个人势 41 | 黑桐亚里亚Official,22074371,个人势 42 | 小毛拔了个火罐,1050301,个人势 43 | 加加Official,37801,个人势 44 | 香取绮罗_kira,22210372,个人势 45 | 是幼情呀,2603963,个人势 46 | 月隐空夜,21292831,个人势 47 | sunyeojin,6632844,个人势 48 | 南宫灯official,664481,个人势 49 | 入間綠,22461463,个人势 50 | 紗耶_sayako,21749031,个人势 51 | 奈奈莉娅channel,22301377,个人势 52 | 涼川越_official,5970661,个人势 53 | 沙月ちゃん,93567,个人势 54 | 草莓奶冻卷,101519,个人势 55 | 贝儿薇尔_Channel,22313544,个人势 56 | 绯卷Himaki,159859,个人势 57 | 味噌缇露official,22613059,个人势 58 | 餅餅結Official,22700650,个人势 59 | 长乐泠Rinnya,893591,个人势 60 | 桃井最中Monaka,22637920,个人势 61 | 子心Koishi,11768825,个人势 62 | 喵田咪芙mifu,9423956,个人势 63 | 姉崎雪美Official,22671782,个人势 64 | 小豆沢AZUKI,1103073471,个人势 65 | 月霖_咕咕,669042,个人势 66 | 望月希咏,21742155,个人势 67 | 紫咲さとり,740509,个人势 68 | 竹花诺特official,22387056,个人势 69 | 盐天使riel_official,8725320,个人势 70 | lz鹿子,360169,个人势 71 | ,, 72 | Overidea_China,704808,Overidea 73 | ,, 74 | AIChannel中国绊爱,21712406,哎咿多工作室 75 | AIChannel官方,1485080,KizunaAI株式会社 76 | love-channel,22219846,KizunaAI株式会社 77 | ,, 78 | 早见咲Saki,22340341,Chobits Live 79 | 钉宫妮妮Ninico,192,Chobits Live 80 | 克莉雅-Koria,22569760,Chobits Live 81 | 立花遥はるか,22791567,Chobits Live 82 | 纪代因果_Inga,22768399,Chobits Live 83 | 秋兔三月,22881847,Chobits Live 84 | ,, 85 | 眞白花音_Official ,21402309,Chucolala 86 | 绯赤艾莉欧Official,21396545,Chucolala 87 | 古守血遊official,8725120,Chucolala 88 | 乙女音Official,21320551,Chucolala 89 | 暗妃鲁咪蕾Official,22671795,Chucolala 90 | 鈴宮鈴,21685677,Chucolala 91 | 紫桃爱音Official,21573665,Chucolala 92 | ,, 93 | 花园Serena,14327465,Paryi Project 94 | 夢乃栞Yumeno_Shiori,14052636,Paryi Project 95 | 椎名菜羽Official,22347054,Paryi Project 96 | 千草はな,12770821,Paryi Project 97 | 帕里_Paryi,4895312,Paryi Project 98 | 有栖Mana_Official,3822389,Paryi Project 99 | 高槻律official,947447,Paryi Project 100 | 森永みうofficial,7962050,Paryi Project 101 | 白雪艾莉娅_Official,22853788,Paryi Project 102 | ,, 103 | 物述有栖Official,21449083,彩虹社 104 | 修女克蕾雅Official,21656036,彩虹社 105 | 家长麦Official,21463227,彩虹社 106 | 叶Official,21449068,彩虹社 107 | 铃木胜Official,22232169,彩虹社 108 | 多拉Official,22376014,彩虹社 109 | 葛叶Official,21449065,彩虹社 110 | 社筑Official,21575222,彩虹社 111 | 鹰宫莉音Official,21463247,彩虹社 112 | 龙胆尊Official,21450685,彩虹社 113 | 雪城真寻Official,22232167,彩虹社 114 | 贝尔蒙德Official,21501730,彩虹社 115 | 绿仙Official,21302479,彩虹社 116 | 森中花咲Official,21463224,彩虹社 117 | 笹木咲Official,21463219,彩虹社 118 | 椎名唯华Official,21302469,彩虹社 119 | 德比鲁Official,21501731,彩虹社 120 | 郡道美玲Official,21575212,彩虹社 121 | 安洁Official,21575228,彩虹社 122 | 戌亥床Official,21575226,彩虹社 123 | 莉泽Official,21463249,彩虹社 124 | 白雪巴_Official,22470202,彩虹社 125 | 健屋花那_Official,22470179,彩虹社 126 | 夜见蕾娜Official,22543160,彩虹社 127 | 本间向日葵Official,21302477,彩虹社 128 | 静凛Official,21302352,彩虹社 129 | 艾克斯Official,21656043,彩虹社 130 | 利维Official,22376018,彩虹社 131 | 樋口枫Official,21449061,彩虹社 132 | 梦月萝娅Official,21656050,彩虹社 133 | 加贺美隼人Official,22470168,彩虹社 134 | 星川莎拉_Official,22470212,彩虹社 135 | 童田明治Official,21575216,彩虹社 136 | 舞元启介Official,21656037,彩虹社 137 | 相羽初叶_Official,22232141,彩虹社 138 | 铃原露露_Official,22470215,彩虹社 139 | 御伽原江良Official,21463238,彩虹社 140 | 纽伊Official,22543132,彩虹社 141 | 黛灰Official,22543135,彩虹社 142 | ,, 143 | 花丸晴琉Official,21547895,花寄女子寮 144 | 鹿乃ちゃん,15152878,花寄女子寮 145 | 小东人魚Official,21547904,花寄女子寮 146 | ,, 147 | 兰音Reine,22696653,虚研社 148 | 小希小桃Channel,4138602,虚研社 149 | 小柔Channel,22696954,虚研社 150 | ,, 151 | 猫芒ベル_Official,21811136,ViViD 152 | 白百合リリィOfficial,21415012,ViViD 153 | 泡沫メモリ_Official,21955596,ViViD 154 | ,, 155 | 田汐汐_Official,21627536,雪风军团 156 | Siva_小虾鱼_,13576775,雪风军团 157 | 伊万_iiivan,22333522,雪风军团 158 | ,, 159 | 白神遥Haruka,21652717,P-SP 160 | 红晓音Akane,411318,P-SP 161 | 秋凛子Rinco,21677969,P-SP 162 | 东爱璃Lovely,21692711,P-SP 163 | 步玎Pudding,21413565,P-SP 164 | 西魔幽Yuu,21775601,P-SP 165 | 北柚香Yuka,22381346,P-SP 166 | 南音乃Nonno,22188174,P-SP 167 | 这是亦枝YY,10317,P-SP 168 | 綾奈奈奈,337374,P-SP 169 | ,, 170 | 进击的冰糖,876396,超电VUP 171 | 新月冰冰,399,超电VUP 172 | 瑠奈luna_Official,22463523,超电VUP 173 | 木糖纯Official,8643223,超电VUP 174 | YUKIri,513,超电VUP 175 | 蝶太,602283,超电VUP 176 | ,, 177 | 早稻叽,631,chaoslive 178 | 白夜真宵Official,21884471,chaoslive 179 | 白夜天存Official,21831694,chaoslive 180 | 牛牛子Channel,377214,chaoslive 181 | ,, 182 | 嘉然今天吃什么,22637261,A-SOUL 183 | 贝拉kira,22632424,A-SOUL 184 | 乃琳Queen,22625027,A-SOUL 185 | 向晚大魔王,22625025,A-SOUL 186 | 珈乐Carol,22634198,A-SOUL 187 | A-SOUL_Official,22632157,A-SOUL 188 | ,, 189 | 千鸟战队_琳,22622469,千鸟战队 190 | 千鸟战队_艾瑞思,22622290,千鸟战队 191 | 千鸟战队_文静,22622535,千鸟战队 192 | 千鸟战队_Coco,22623163,千鸟战队 193 | 千鸟战队_艾白,13747625,千鸟战队 194 | 千鸟战队,22622904,千鸟战队 195 | ,, 196 | 满月Channel,21570295,Providence 企划 197 | 花花Haya,7688602,Providence 企划 198 | 胡桃Usa,4788550,Providence 企划 199 | 柊真华Official,22251977,Providence 企划 200 | 咲间妮娜Official,22566228,Providence 企划 201 | 七濑Unia,21514463,Providence 企划 202 | 玛安娜Myanna,22321043,Providence 企划 203 | 毬亚Maria,22323445,Providence 企划 204 | 阿伊蕾特Ayelet,11306,Providence 企划 205 | ,, 206 | 电脑少女siro_小白,21307497,.live 207 | ,, 208 | 天川花乃official,21693692,Re:AcT 209 | 白音雪-official,22319849,Re:AcT 210 | 姬熊结璃Official,21693691,Re:AcT 211 | 狮子神蕾欧娜_official,21622698,Re:AcT 212 | 黑音夜见Official,22310216,Re:AcT 213 | ,, 214 | 御剣莉亚-官方,22055164,WACTOR 215 | 日月咪玉-官方,21300316,WACTOR 216 | 月兔华-WACTOR官方,21720301,WACTOR 217 | 木暮Piyoko-官方,21720308,WACTOR 218 | 七葉琉伊,21779064,WACTOR 219 | ,, 220 | 雨街F,4611671,RedCircle 221 | 亚哈Ahab_Channel,21398753,RedCircle 222 | 凛星RinStar,14507014,RedCircle 223 | ,, 224 | 犬山玉姬Official,4634167,Noripro 225 | 白雪深白Official,21708004,Noripro 226 | 爱宫Milk_Official,22111399,Noripro 227 | 姬咲柚流Official,22534525,Noripro 228 | ,, 229 | 战斗吧歌姬官方账号,14578426,战斗吧歌姬 230 | ,, 231 | 泠鸢yousa,593,VirtuaReal 232 | 七海Nana7mi,21452505,VirtuaReal 233 | 阿萨Aza,21696950,VirtuaReal 234 | 琉绮Ruki,21403609,VirtuaReal 235 | 中单光一,21457197,VirtuaReal 236 | 艾因Eine,21403601,VirtuaReal 237 | 千春_Chiharu,22389319,VirtuaReal 238 | 安堂いなり_official,21224291,VirtuaReal 239 | 菜菜子Nanako,22359795,VirtuaReal 240 | 菫妃奈Official,4418655,VirtuaReal 241 | 罗伊_Roi,21696953,VirtuaReal 242 | 花留Karu,22111428,VirtuaReal 243 | 祖娅纳惜,938957,VirtuaReal 244 | 清良Kiyora,22389323,VirtuaReal 245 | 千幽Chiyuu,22605464,VirtuaReal 246 | ,, 247 | 熊野ぽえみOfficial,22538603,zeroproject 248 | 紡音零official,22552250,zeroproject 249 | ,, 250 | 辻蓝佳音瑠_channel,22439372,柑橘社 251 | 天使沙依_channel,22424371,柑橘社 252 | 米艾尔蜂蜜Official,22313051,柑橘社 253 | 梱枝莉子Official,22595542,柑橘社 254 | ,, 255 | 月见_tsuki,21417013,无次元 256 | ,, 257 | 切茜娅CheIsea,1944820,魔女 258 | ,, 259 | 杜松子_Gin,22195814,猫邮社 260 | 猫小鹰Owl,22197280,猫邮社 261 | ,, 262 | 五牙Kiba,529865,EGOLIVE 263 | 克莉斯多_Crystal,74905912,EGOLIVE 264 | 宇佐紀ノノ_usagi,313248263,EGOLIVE 265 | 小怪兽RUARUA,581965619,EGOLIVE 266 | 橘橘子channel,3923305,EGOLIVE 267 | 水原塔可Offical,557794975,EGOLIVE 268 | 莱妮娅_Rynia,703018634,EGOLIVE 269 | 雨宫燕Channel,673595,EGOLIVE 270 | 鮫島愛知Aichi,491652986,EGOLIVE 271 | 黑见来世Channel,1512180333,EGOLIVE 272 | ,, 273 | 乌拉の帝国Official,14893,lucca事务所 274 | ,, 275 | 余生channel,22432741,北坊 276 | 梦舞风云Channel,8180987,北坊 277 | 荼茜ゲル_official,321224,北坊 278 | 吸溜君,590990,北坊 279 | 莫宇声含Channel,8170722,北坊 280 | 伊格_Eagle,22272834,北坊 281 | 月璃Yori-Channel,2099492,北坊 282 | ,, 283 | 田中姬铃木雏Official,10209381,HIMEHINA 284 | --------------------------------------------------------------------------------