├── .gitignore ├── README.md ├── area_id.txt ├── config.ini ├── install.sh ├── liveInfo.json ├── main.py ├── oldFile ├── bilibili.py ├── utils.py └── youtube.py ├── refresh_area_id.py ├── schedule.txt ├── src ├── Configs.py ├── __init__.py ├── liveScheduler.py ├── login_bilibili.py ├── makeLive.py ├── methods │ ├── __init__.py │ ├── m_bilibili.py │ └── m_youtube.py ├── rebroadcast.py ├── utitls.py └── webSite │ ├── __init__.py │ ├── models │ └── autoLive.py │ ├── static │ ├── css │ │ ├── cascade.css │ │ └── mobile.css │ ├── midokure.ico │ └── scripts │ │ ├── cbi.js │ │ └── xhr.js │ ├── templates │ ├── configs.html │ ├── layout.html │ └── scheduler.html │ └── views │ └── autoLive.py └── updateConfigs.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # test file 107 | cookies.txt 108 | log.txt 109 | err.log 110 | test.py 111 | .removed/ 112 | *~ 113 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # autoLive 2 | 3 | ## 简介 4 | 利用[APScheduler][APScheduler],按时间表定时启动转播任务。 5 | 6 | 利用[flask框架][flask]构建浏览器端,动态查询或增删时间表中的转播任务。 7 | 8 | 一个转播任务中,利用[youtube-dl][youtube-dl]获得YouTube直播m3u8地址,并用[ffmpeg][ffmpeg]将对应直播转播到B站直播。 9 | 10 | 程序运行于`Python3`。 11 | 12 | ## 一键安装脚本 13 | - 以下所有步骤中的粘贴不能使用ctrl+v!应使用右键->粘贴。 14 | - 将以下几行命令粘贴入Xshell,自动下载安装脚本并运行: 15 | ```bash 16 | cd ~ # 进入/root/文件夹中。install.sh默认在此目录中运行。 17 | rm -f install.sh # 移除上次安装遗留的install.sh文件 18 | sudo yum install -y wget # 如果系统中不自带wget,则安装wget。 19 | wget https://raw.githubusercontent.com/RyoJerryYu/autoLive/master/install.sh # 下载安装脚本install.sh 20 | chmod +x install.sh # 给与安装脚本权限 21 | bash install.sh # 运行安装脚本 22 | ``` 23 | - 运行大概3分钟左右后,会出现如下提示: 24 | ``` 25 | ############################### 26 | # # 27 | # paste your bilibili cookies # 28 | # # 29 | ############################### 30 | ``` 31 | - 此时将直播所用的账号的cookie串粘贴,回车继续运行 32 | - 出现输入端口的提示后,输入0~65535间的一个数字,此数字即为下面所用的端口号。注意不要跟常用的80、22等端口冲突。如不知道怎样的端口会冲突,推荐使用2434端口。 33 | 34 | - 出现`安装终了`的提示后,安装完成。现在一键安装脚本不会自动启动程序,请按下面步骤在screen中启动程序。 35 | - 逐行输入以下命令: 36 | ```bash 37 | screen -S autoLive # 打开新的screen窗口并命名为autoLive。在screen中启动的程序即使关闭shell也能继续运行。 38 | cd ~/autoLive # 进入autoLive文件夹。本程序必须从autoLive文件夹中打开。 39 | python36 main.py # 使用python3.6运行本程序。 40 | ``` 41 | - 程序运行后,先按`ctrl+A`,然后按下`d`键退出screen窗口。此时在screen中启动的程序仍会在后台运行。 42 | - 然后使用浏览器登陆`http://<服务器ip>:<端口号>/autoLive/`,如果没有报错,则安装成功。 43 | 44 | - 停止程序时请按以下步骤停止: 45 | - 输入以下命令: 46 | ```bash 47 | screen -r autoLive # 重新打开名为autoLive的screen窗口 48 | ``` 49 | - 按下`ctrl+C`停止程序。如果有转播任务正在运行可能需要多按几次。 50 | - 输入以下命令: 51 | ```bash 52 | exit # 退出并关闭screen窗口。 53 | ``` 54 | - 程序停止后可按上面启动程序的步骤重新启动。 55 | - 后续更新时只需先停止程序,然后重新运行最初的几行命令即可。 56 | - 默认自带的VTuber只包括にじさんじ。如果需要转播其他VTuber需要手动修改`liveInfo.json`,格式请参照下面`liveInfo.json`的格式。 57 | 58 | ## 测试环境 59 | - CentOS 7 60 | - youtube-dl 2018.10.05 61 | - ffmpeg 4.0.2-static 62 | - Python 3.6 63 | - requests 2.19.1 64 | - APScheduler 3.5.3 65 | - Flask 1.0.2 66 | 67 | ## 依赖与安装 68 | #### 软件依赖 69 | 安装`youtube-dl`,`ffmpeg`,`python3`。如果系统中没有`wget`需要事先安装`wget`。 70 | ```bash 71 | # 安装youtube-dl 72 | wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl 73 | chmod a+rx /usr/local/bin/youtube-dl 74 | # 安装ffmpeg 75 | wget https://raw.githubusercontent.com/q3aql/ffmpeg-install/master/ffmpeg-install 76 | chmod a+x ffmpeg-install 77 | ./ffmpeg-install --install release 78 | # 安装python3 79 | sudo yum install python3 80 | ``` 81 | 82 | #### Python库依赖 83 | 安装`requests`,`APScheduler`,`Flask`。需要先安装python3对应的pip。 84 | ```bash 85 | # 安装requests 86 | pip3 install requests 87 | # 安装APScheduler 88 | pip3 install APScheduler 89 | # 安装Flask 90 | pip3 install Flask 91 | ``` 92 | 93 | ## 运行 94 | - 先使用`git clone`把代码clone到本地,并cd到对应`autoLive`目录中。需要先安装git。 95 | - 把B站cookies串粘贴到`autoLive/cookies.txt`中。 96 | 此时可运行`python3 refresh_area_id.py`更新area_id.txt,同时测试cookies串是否能登陆。 97 | ```bash 98 | python3 refresh_area_id.py 99 | ``` 100 | - 配置`config.ini`,主要根据自己情况更改直播间标题格式、分区及最高清晰度。其中`{time}``{liver}``{site}``{title}`均为时间表中的参数。 101 | - (可选)填写`schedule.txt`。目前只能解析未来24小时内的直播,而且每次重新运行都需要读取一次时间表。因为时间表增删也可从网页端设置,此步可忽略。 102 | ``` 103 | # 时间表格式: 104 | # time@liver@site@title 105 | # time:直播时间,可填入"now",会自动认为在读取时间表30秒后开播。 106 | # 此外只能填入HHMM四位数字。 107 | # 只接受未来24小时内的直播,检测出直播时间在运行程序之前时,会自动认为直播在运行程序第二天开始。 108 | # liver:只接受liveInfo.json中存在的liver名。(可自行按json格式添加到liveInfo文件中) 109 | # site:直播网站,可选。目前只接受YouTube。而且不填默认为YouTube。 110 | # title:填入config.ini中直播间标题的title中,可选,不填默认为"{liver}"+"转播" 111 | 112 | # 例: 113 | # 1900@桜凛月@绝地求生 114 | # 2100@黒井しば 115 | # 2240@飛鳥ひな@YouTube 116 | # 0640@伏見ガク@YouTube@おはガク! 117 | ``` 118 | 119 | 其中liver只能接受`liveInfo.json`中存在的liver,可自行按照对应格式添加新直播主至`liveInfo.json`中。格式如下: 120 | 121 | ```python 122 | [ # list,每一项dictionary对应一位liver 123 | { 124 | "liver": "樋口楓", # str,与时间表中liver项对应 125 | "room": [ # list,每一项dictionary对应一个直播间 126 | { 127 | "site": "YouTube", # str,直播网站名,注意大小写 128 | "url": "https://www.youtube.com/channel/UCsg-YqdqQ-KFF0LNk23BY4A/live" # str,对应直播间url 129 | }, 130 | # 其他网站的直播间 131 | # 但是目前只能解析YouTube上的直播间,所以list的其他项没有意义 132 | ] 133 | }, 134 | # 相同格式的其他liver信息 135 | ] 136 | ``` 137 | 138 | - 运行`main.py`。建议在screen中启动。 139 | ```bash 140 | python3 main.py 141 | ``` 142 | - 登入网页端。`:2434/autoLive/`,其中ip地址为服务器ip地址。按需要添加或删除时间项。 143 | 144 | 时间一栏可填入以下内容: 145 | 1. "now": 程序会自动将开播时间定为点击提交的30秒后。 146 | 2. 填入HHMM格式的四位数字,代表未来24小时内对应日本时区的时间。 147 | 目前只能接受未来24小时内开始的直播。 148 | 149 | '自定义标题'可不填,不填时默认为"{liver}"+"转播" 150 | 151 | - 准备完成,现在只需等待程序自动转播到你的B站账号了! 152 | 153 | ## 已进行过的修改 154 | - 需要手动填写时间表,手动传输时间表到服务器并手动启动程序。 155 | - 程序不能一直运行,需要每天启动读取时间表。 156 | - 灵活度不足。时间表一旦运行后不能增删改,无法应对突击直播等情况。 157 | 158 | 以上三点已通过添加网页端,并在网页端上进行时间表的增删改来解决。 159 | 160 | 161 | ## 缺点与修改方案 162 | - 设置不够灵活,只能通过config.ini手动修改 163 | 164 | 以后会增加在网页端进行设置、添加liveInfo等功能 165 | 166 | - 只能接受YouTube上的直播 167 | 168 | 已预留好接口,以后可进行其他网站直播的扩充。 169 | 170 | - 直播前只能发文字动态,不能转发直播间 171 | 172 | 虽未获得转发直播间的API,但已通过增加直播间链接的方法暂时解决。 173 | 174 | - 同时只能通过一个B站账号进行转播 175 | 176 | 以后可能会增加多账号支持,但考虑到VPS的承受能力,不推荐同一服务器同时进行多项转播。 177 | 178 | ## TODO LIST 179 | - [X] 可视化网页端 180 | - [X] 网页端进行时间表增删改查 181 | - [X] 每日自动发送每日时间表 182 | - [X] 退出程序时保存时间表 183 | - [X] 网页端现在可以查看正在运行中的项目了 184 | - [X] 增加网页端设置页面 185 | - [ ] 增加可以设置的项目 186 | - [ ] 可以从网页端增删liveInfo 187 | - [ ] 修正因每日动态过长而无法发送问题 188 | - [ ] 尝试使用streamlink代替ffmpeg 189 | - [ ] 其他网站支持 190 | 191 | ## 特别鸣谢 192 | - B站id: 一生的等待 193 | - [HalfMAI/AutoYtB](https://github.com/HalfMAI/AutoYtB) 194 | - [7rikka/autoLive](https://github.com/7rikka/autoLive) 195 | - [pandaGao/bilibili-live](https://github.com/pandaGao/bilibili-live) 196 | 197 | 本程序有参考以上个人或项目的思路或代码内容,遇到困难时也得到了他们的援助,在此表示感谢。 198 | 199 | 200 | [APScheduler]: https://apscheduler.readthedocs.io/en/latest/ 201 | [youtube-dl]: https://youtube-dl.org/ 202 | [ffmpeg]: https://www.ffmpeg.org/ 203 | [flask]: http://flask.pocoo.org/ -------------------------------------------------------------------------------- /area_id.txt: -------------------------------------------------------------------------------- 1 | 最终修改于:2018 10.15 23:10 2 | 3 | 1.娱乐 4 | 123.户外 5 | 136.美食 6 | 143.才艺 7 | 145.视频聊天 8 | 160.唱见电台 9 | 161.催眠电台 10 | 162.聊天电台 11 | 21.视频唱见 12 | 25.手工 13 | 27.学习 14 | 28.萌宠 15 | 30.视频催眠 16 | 33.映评馆 17 | 34.音乐台 18 | 2.游戏 19 | 102.最终幻想14 20 | 107.其他游戏 21 | 112.龙之谷 22 | 114.风暴英雄 23 | 115.坦克世界 24 | 138.超级马里奥奥德赛 25 | 147.怪物猎人:世界 26 | 164.堡垒之夜 27 | 166.逆水寒 28 | 167.灵魂筹码 29 | 172.武侠乂 30 | 173.古剑奇谭OL 31 | 176.幻想全明星 32 | 179.荒野行动Plus 33 | 181.魔兽争霸3 34 | 183.太吾绘卷 35 | 185.中国式家长 36 | 56.我的世界 37 | 57.以撒 38 | 64.饥荒 39 | 78.DNF 40 | 80.绝地求生 41 | 81.三国杀 42 | 82.剑网3 43 | 83.魔兽世界 44 | 84.300英雄 45 | 86.英雄联盟 46 | 87.守望先锋 47 | 88.穿越火线 48 | 89.CS:GO 49 | 90.CS 50 | 91.炉石传说 51 | 92.DOTA2 52 | 93.星际争霸2 53 | 3.手游 54 | 113.碧蓝航线 55 | 140.决战!平安京 56 | 141.荒野行动 57 | 153.绝地求生:刺激战场 58 | 154.QQ飞车 59 | 155.绝地求生:全军出击 60 | 156.影之诗 61 | 163.第五人格 62 | 174.电击文库:零境交错 63 | 178.梦幻模拟战 64 | 182.方舟指令 65 | 184.神都夜行录 66 | 35.王者荣耀 67 | 36.阴阳师 68 | 37.Fate/GO 69 | 39.少女前线 70 | 40.崩坏3 71 | 41.狼人杀 72 | 42.解密游戏 73 | 48.虚荣 74 | 50.部落冲突:皇室战争 75 | 98.其他手游 76 | 4.绘画 77 | 51.原创绘画 78 | 94.同人绘画 79 | 95.临摹绘画 80 | 96.其他绘画 81 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [basic] 2 | # 版本信息,更新时会读取此项决定设置项保存方法 3 | VERSION = 2.1.0 4 | # 网页端端口 5 | WEB_PORT = 2434 6 | # 账号cookies 7 | COOKIES_TXT_PATH = ./cookies.txt 8 | # 时间表schedule 9 | SCHEDULE_TXT_PATH = ./schedule.txt 10 | # 直播源信息 11 | LIVE_INFO_PATH = ./liveInfo.json 12 | # 推流命令 13 | FFMPEG_COMMAND = ffmpeg -i "{}" -vcodec copy -acodec aac -strict -2 -ac 2 -bsf:a aac_adtstoasc -flags +global_header -f flv "{}" 14 | 15 | [bilibili] 16 | # 直播间标题 17 | # 可使用参数: {time}, {liver}, {site}, {title} 18 | BILIBILI_ROOM_TITLE = 【{liver}】{title} 转播 19 | # 默认title参数 20 | # 可使用参数: {time}, {liver}, {site} 21 | DEFAULT_TITLE_PARAM = {liver} {time} 22 | # 直播间分区id,请查询area_id.txt 23 | BILIBILI_ROOM_AREA_ID = 33 24 | # 是否发送每日动态 25 | IS_SEND_DAILY_DYNAMIC = true 26 | # 每日动态中每行的格式 27 | # 可使用参数:{time}, {liver}, {site}, {title} 28 | DAILY_DYNAMIC_FORM = {time}, {liver}, {site}\n{title}\n\n 29 | # 是否发送直播前动态 30 | IS_SEND_PRELIVE_DYNAMIC = true 31 | # 直播前动态格式 32 | # 可使用参数:{time}, {liver}, {site}, {title}, {url} 33 | # 其中url项为B站直播间链接 34 | PRELIVE_DYNAMIC_FORM = 开始转播:{liver}\n时间:{time}\n{title}\n{url} 35 | 36 | [liveParam] 37 | # 最高清晰度 推荐数值:240 360 480 720 1080 38 | # 转播清晰度不会高于此值,为0时不设最高清晰度 39 | LIVE_QUALITY = 480 40 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | sudo yum install -y epel-release 3 | sudo yum update -y 4 | 5 | sudo rpm --import http://li.nux.ro/download/nux/RPM-GPG-KEY-nux.ro 6 | sudo rpm -Uvh http://li.nux.ro/download/nux/dextop/el7/x86_64/nux-dextop-release-0-5.el7.nux.noarch.rpm 7 | 8 | sudo yum install -y wget 9 | 10 | wget https://yt-dl.org/downloads/latest/youtube-dl -O /usr/local/bin/youtube-dl 11 | chmod a+rx /usr/local/bin/youtube-dl 12 | 13 | sudo yum install -y python36 14 | 15 | wget https://bootstrap.pypa.io/get-pip.py 16 | sudo python36 get-pip.py 17 | rm -f get-pip.py 18 | 19 | pip3 install --upgrade pip 20 | pip3 install requests 21 | pip3 install apscheduler 22 | pip3 install flask 23 | 24 | firewall-cmd --zone=public --add-port=2434/tcp --permanent 25 | firewall-cmd --reload 26 | 27 | sudo yum install -y git 28 | 29 | cd ~ 30 | wget https://raw.githubusercontent.com/Sporesirius/ffmpeg-install/master/ffmpeg-install 31 | chmod a+x ffmpeg-install 32 | ./ffmpeg-install --install release 33 | rm -f ffmpeg-install 34 | 35 | cd ~ 36 | mkdir autoLiveTemp 37 | mv -f autoLive/*.json autoLiveTemp/ 38 | rm -rf autoLive 39 | git clone https://github.com/RyoJerryYu/autoLive.git 40 | mv -f autoLiveTemp/*.json autoLive/ 41 | rm -rf autoLiveTemp 42 | 43 | sudo yum install -y screen 44 | 45 | cd ~/autoLive 46 | python36 updateConfigs.py -------------------------------------------------------------------------------- /liveInfo.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "liver": " 月ノ美兎", 4 | "room": [ 5 | { 6 | "site": "YouTube", 7 | "url": "https://www.youtube.com/channel/UCD-miitqNY3nyukJ4Fnf4_A/live" 8 | } 9 | ] 10 | }, 11 | { 12 | "liver": "樋口楓", 13 | "room": [ 14 | { 15 | "site": "YouTube", 16 | "url": "https://www.youtube.com/channel/UCsg-YqdqQ-KFF0LNk23BY4A/live" 17 | } 18 | ] 19 | }, 20 | { 21 | "liver": "静凛", 22 | "room": [ 23 | { 24 | "site": "YouTube", 25 | "url": "https://www.youtube.com/channel/UC6oDys1BGgBsIC3WhG1BovQ/live" 26 | } 27 | ] 28 | }, 29 | { 30 | "liver": "える", 31 | "room": [ 32 | { 33 | "site": "YouTube", 34 | "url": "https://www.youtube.com/channel/UCYKP16oMX9KKPbrNgo_Kgag/live" 35 | } 36 | ] 37 | }, 38 | { 39 | "liver": "モイラ", 40 | "room": [ 41 | { 42 | "site": "YouTube", 43 | "url": "https://www.youtube.com/channel/UCvmppcdYf4HOv-tFQhHHJMA/live" 44 | } 45 | ] 46 | }, 47 | { 48 | "liver": "勇気千尋", 49 | "room": [ 50 | { 51 | "site": "YouTube", 52 | "url": "https://www.youtube.com/channel/UCLO9QDxVL4bnvRRsz6K4bsQ/live" 53 | } 54 | ] 55 | }, 56 | { 57 | "liver": "鈴谷アキ", 58 | "room": [ 59 | { 60 | "site": "YouTube", 61 | "url": "https://www.youtube.com/channel/UCt9qik4Z-_J-rj3bKKQCeHg/live" 62 | } 63 | ] 64 | }, 65 | { 66 | "liver": "渋谷ハジメ", 67 | "room": [ 68 | { 69 | "site": "YouTube", 70 | "url": "https://www.youtube.com/channel/UCeK9HFcRZoTrvqcUCtccMoQ/live" 71 | } 72 | ] 73 | }, 74 | { 75 | "liver": "物述有栖", 76 | "room": [ 77 | { 78 | "site": "YouTube", 79 | "url": "https://www.youtube.com/channel/UCt0clH12Xk1-Ej5PXKGfdPA/live" 80 | } 81 | ] 82 | }, 83 | { 84 | "liver": "鈴鹿詩子", 85 | "room": [ 86 | { 87 | "site": "YouTube", 88 | "url": "https://www.youtube.com/channel/UCwokZsOK_uEre70XayaFnzA/live" 89 | } 90 | ] 91 | }, 92 | { 93 | "liver": "剣持刀也", 94 | "room": [ 95 | { 96 | "site": "YouTube", 97 | "url": "https://www.youtube.com/channel/UCv1fFr156jc65EMiLbaLImw/live" 98 | } 99 | ] 100 | }, 101 | { 102 | "liver": "家長むぎ", 103 | "room": [ 104 | { 105 | "site": "YouTube", 106 | "url": "https://www.youtube.com/channel/UC_GCs6GARLxEHxy1w40d6VQ/live" 107 | } 108 | ] 109 | }, 110 | { 111 | "liver": "宇志海いちご", 112 | "room": [ 113 | { 114 | "site": "YouTube", 115 | "url": "https://www.youtube.com/channel/UCmUjjW5zF1MMOhYUwwwQv9Q/live" 116 | } 117 | ] 118 | }, 119 | { 120 | "liver": "森中花咲", 121 | "room": [ 122 | { 123 | "site": "YouTube", 124 | "url": "https://www.youtube.com/channel/UCtpB6Bvhs1Um93ziEDACQ8g/live" 125 | } 126 | ] 127 | }, 128 | { 129 | "liver": "夕陽リリ", 130 | "room": [ 131 | { 132 | "site": "YouTube", 133 | "url": "https://www.youtube.com/channel/UC48jH1ul-6HOrcSSfoR02fQ/live" 134 | } 135 | ] 136 | }, 137 | { 138 | "liver": "文野環", 139 | "room": [ 140 | { 141 | "site": "YouTube", 142 | "url": "https://www.youtube.com/channel/UCBiqkFJljoxAj10SoP2w2Cg/live" 143 | } 144 | ] 145 | }, 146 | { 147 | "liver": "伏見ガク", 148 | "room": [ 149 | { 150 | "site": "YouTube", 151 | "url": "https://www.youtube.com/channel/UCXU7YYxy_iQd3ulXyO-zC2w/live" 152 | } 153 | ] 154 | }, 155 | { 156 | "liver": "ギルザレンIII", 157 | "room": [ 158 | { 159 | "site": "YouTube", 160 | "url": "https://www.youtube.com/channel/UCUzJ90o1EjqUbk2pBAy0_aw/live" 161 | } 162 | ] 163 | }, 164 | { 165 | "liver": "岩永", 166 | "room": [ 167 | { 168 | "site": "YouTube", 169 | "url": "https://www.youtube.com/channel/UCGCb4Dts1uYtciya3wvd6dg/live" 170 | } 171 | ] 172 | }, 173 | { 174 | "liver": "にじさんじ", 175 | "room": [ 176 | { 177 | "site": "YouTube", 178 | "url": "https://www.youtube.com/channel/UCX7YkU9nEeaoZbkVLVajcMg/live" 179 | } 180 | ] 181 | }, 182 | { 183 | "liver": "叶", 184 | "room": [ 185 | { 186 | "site": "YouTube", 187 | "url": "https://www.youtube.com/channel/UCspv01oxUFf_MTSipURRhkA/live" 188 | } 189 | ] 190 | }, 191 | { 192 | "liver": "赤羽葉子", 193 | "room": [ 194 | { 195 | "site": "YouTube", 196 | "url": "https://www.youtube.com/channel/UCBi8YaVyZpiKWN3_Z0dCTfQ/live" 197 | } 198 | ] 199 | }, 200 | { 201 | "liver": "笹木咲", 202 | "room": [ 203 | { 204 | "site": "YouTube", 205 | "url": "https://www.youtube.com/channel/UCoztvTULBYd3WmStqYeoHcA/live" 206 | } 207 | ] 208 | }, 209 | { 210 | "liver": "本間ひまわり", 211 | "room": [ 212 | { 213 | "site": "YouTube", 214 | "url": "https://www.youtube.com/channel/UC0g1AE0DOjBYnLhkgoRWN1w/live" 215 | } 216 | ] 217 | }, 218 | { 219 | "liver": "闇夜乃モルル", 220 | "room": [ 221 | { 222 | "site": "YouTube", 223 | "url": "https://www.youtube.com/channel/UCNUgrFCo2Hr_VXc9bEwjcHQ/live" 224 | } 225 | ] 226 | }, 227 | { 228 | "liver": "葛葉", 229 | "room": [ 230 | { 231 | "site": "YouTube", 232 | "url": "https://www.youtube.com/channel/UCSFCh5NL4qXrAy9u-u2lX3g/live" 233 | } 234 | ] 235 | }, 236 | { 237 | "liver": "雪汝", 238 | "room": [ 239 | { 240 | "site": "YouTube", 241 | "url": "https://www.youtube.com/channel/UCfM_A7lE6LkGrzx6_mOtI4g/live" 242 | } 243 | ] 244 | }, 245 | { 246 | "liver": "椎名唯華", 247 | "room": [ 248 | { 249 | "site": "YouTube", 250 | "url": "https://www.youtube.com/channel/UC_4tXjqecqox5Uc05ncxpxg/live" 251 | } 252 | ] 253 | }, 254 | { 255 | "liver": "魔界ノりりむ", 256 | "room": [ 257 | { 258 | "site": "YouTube", 259 | "url": "https://www.youtube.com/channel/UC9EjSJ8pvxtvPdxLOElv73w/live" 260 | } 261 | ] 262 | }, 263 | { 264 | "liver": "シスター・クレア", 265 | "room": [ 266 | { 267 | "site": "YouTube", 268 | "url": "https://www.youtube.com/channel/UC1zFJrfEKvCixhsjNSb1toQ/live" 269 | } 270 | ] 271 | }, 272 | { 273 | "liver": "ドーラ", 274 | "room": [ 275 | { 276 | "site": "YouTube", 277 | "url": "https://www.youtube.com/channel/UC53UDnhAAYwvNO7j_2Ju1cQ/live" 278 | } 279 | ] 280 | }, 281 | { 282 | "liver": "緑仙", 283 | "room": [ 284 | { 285 | "site": "YouTube", 286 | "url": "https://www.youtube.com/channel/UCt5-0i4AVHXaWJrL8Wql3mw/live" 287 | } 288 | ] 289 | }, 290 | { 291 | "liver": "海夜叉神", 292 | "room": [ 293 | { 294 | "site": "YouTube", 295 | "url": "https://www.youtube.com/channel/UCqEp6RdtsMbUNrCdCswr6pA/live" 296 | } 297 | ] 298 | }, 299 | { 300 | "liver": "花畑チャイカ", 301 | "room": [ 302 | { 303 | "site": "YouTube", 304 | "url": "https://www.youtube.com/channel/UCsFn_ueskBkMCEyzCEqAOvg/live" 305 | } 306 | ] 307 | }, 308 | { 309 | "liver": "社築", 310 | "room": [ 311 | { 312 | "site": "YouTube", 313 | "url": "https://www.youtube.com/channel/UCKMYISTJAQ8xTplUPHiABlA/live" 314 | } 315 | ] 316 | }, 317 | { 318 | "liver": "卯月コウ", 319 | "room": [ 320 | { 321 | "site": "YouTube", 322 | "url": "https://www.youtube.com/channel/UC3lNFeJiTq6L3UWoz4g1e-A/live" 323 | } 324 | ] 325 | }, 326 | { 327 | "liver": "出雲霞", 328 | "room": [ 329 | { 330 | "site": "YouTube", 331 | "url": "https://www.youtube.com/channel/UCLpYMk5h1bq8_GAFVBgXhPQ/live" 332 | } 333 | ] 334 | }, 335 | { 336 | "liver": "八朔ゆず", 337 | "room": [ 338 | { 339 | "site": "YouTube", 340 | "url": "https://www.youtube.com/channel/UCFaDvgez8USXHiKidt0NtZg/live" 341 | } 342 | ] 343 | }, 344 | { 345 | "liver": "鈴木勝", 346 | "room": [ 347 | { 348 | "site": "YouTube", 349 | "url": "https://www.youtube.com/channel/UCaF-mODqAziHivqg0Q61XKQ/live" 350 | } 351 | ] 352 | }, 353 | { 354 | "liver": "名伽尾アズマ", 355 | "room": [ 356 | { 357 | "site": "YouTube", 358 | "url": "https://www.youtube.com/channel/UCks41vQN-hN-1KHmpZyPY3A/live" 359 | } 360 | ] 361 | }, 362 | { 363 | "liver": "轟京子", 364 | "room": [ 365 | { 366 | "site": "YouTube", 367 | "url": "https://www.youtube.com/channel/UCRV9d6YCYIMUszK-83TwxVA/live" 368 | } 369 | ] 370 | }, 371 | { 372 | "liver": "安土桃", 373 | "room": [ 374 | { 375 | "site": "YouTube", 376 | "url": "https://www.youtube.com/channel/UC6TfqY40Xt1Y0J-N18c85qQ/live" 377 | } 378 | ] 379 | }, 380 | { 381 | "liver": "鷹宮リオン", 382 | "room": [ 383 | { 384 | "site": "YouTube", 385 | "url": "https://www.youtube.com/channel/UCV5ZZlLjk5MKGg3L0n0vbzw/live" 386 | } 387 | ] 388 | }, 389 | { 390 | "liver": "舞元啓介", 391 | "room": [ 392 | { 393 | "site": "YouTube", 394 | "url": "https://www.youtube.com/channel/UCJubINhCcFXlsBwnHp0wl_g/live" 395 | } 396 | ] 397 | }, 398 | { 399 | "liver": "飛鳥ひな", 400 | "room": [ 401 | { 402 | "site": "YouTube", 403 | "url": "https://www.youtube.com/channel/UCiSRx1a2k-0tOg-fs6gAolQ/live" 404 | } 405 | ] 406 | }, 407 | { 408 | "liver": "雨森小夜", 409 | "room": [ 410 | { 411 | "site": "YouTube", 412 | "url": "https://www.youtube.com/channel/UCRWOdwLRsenx2jLaiCAIU4A/live" 413 | } 414 | ] 415 | }, 416 | { 417 | "liver": "春崎エアル", 418 | "room": [ 419 | { 420 | "site": "YouTube", 421 | "url": "https://www.youtube.com/channel/UCtAvQ5U0aXyKwm2i4GqFgJg/live" 422 | } 423 | ] 424 | }, 425 | { 426 | "liver": "鳴門こがね", 427 | "room": [ 428 | { 429 | "site": "YouTube", 430 | "url": "https://www.youtube.com/channel/UCF1JdALrXgub24weQpqDy9Q/live" 431 | } 432 | ] 433 | }, 434 | { 435 | "liver": "神田笑一", 436 | "room": [ 437 | { 438 | "site": "YouTube", 439 | "url": "https://www.youtube.com/channel/UCWz0CSYCxf4MhRKPDm220AQ/live" 440 | } 441 | ] 442 | }, 443 | { 444 | "liver": "ジョー・力一", 445 | "room": [ 446 | { 447 | "site": "YouTube", 448 | "url": "https://www.youtube.com/channel/UChUJbHiTVeGrSkTdBzVfNCQ/live" 449 | } 450 | ] 451 | }, 452 | { 453 | "liver": "竜胆尊", 454 | "room": [ 455 | { 456 | "site": "YouTube", 457 | "url": "https://www.youtube.com/channel/UCPvGypSgfDkVe7JG2KygK7A/live" 458 | } 459 | ] 460 | }, 461 | { 462 | "liver": "でびでび・でびる", 463 | "room": [ 464 | { 465 | "site": "YouTube", 466 | "url": "https://www.youtube.com/channel/UCP19rQ5V-46B-6ZeLDJqp5w/live" 467 | } 468 | ] 469 | }, 470 | { 471 | "liver": "桜凛月", 472 | "room": [ 473 | { 474 | "site": "YouTube", 475 | "url": "https://www.youtube.com/channel/UCfQVs_KuXeNAlGa3fb8rlnQ/live" 476 | } 477 | ] 478 | }, 479 | { 480 | "liver": "町田ちま", 481 | "room": [ 482 | { 483 | "site": "YouTube", 484 | "url": "https://www.youtube.com/channel/UCo7TRj3cS-f_1D9ZDmuTsjw/live" 485 | } 486 | ] 487 | }, 488 | { 489 | "liver": "月見しずく", 490 | "room": [ 491 | { 492 | "site": "YouTube", 493 | "url": "https://www.youtube.com/channel/UCqQV8xEBWd5SVZBLlYrS_5Q/live" 494 | } 495 | ] 496 | }, 497 | { 498 | "liver": "遠北千南", 499 | "room": [ 500 | { 501 | "site": "YouTube", 502 | "url": "https://www.youtube.com/channel/UCuz0vzQgC8LRdS6lVV0UkUg/live" 503 | } 504 | ] 505 | }, 506 | { 507 | "liver": "夢追翔", 508 | "room": [ 509 | { 510 | "site": "YouTube", 511 | "url": "https://www.youtube.com/channel/UCTIE7LM5X15NVugV7Krp9Hw/live" 512 | } 513 | ] 514 | }, 515 | { 516 | "liver": "黒井しば", 517 | "room": [ 518 | { 519 | "site": "YouTube", 520 | "url": "https://www.youtube.com/channel/UCmeyo5pRj_6PXG-CsGUuWWg/live" 521 | } 522 | ] 523 | }, 524 | { 525 | "liver": "ベルモンド・バンデラス", 526 | "room": [ 527 | { 528 | "site": "YouTube", 529 | "url": "https://www.youtube.com/channel/UCbc8fwhdUNlqi-J99ISYu4A/live" 530 | } 531 | ] 532 | }, 533 | { 534 | "liver": "成瀬鳴", 535 | "room": [ 536 | { 537 | "site": "YouTube", 538 | "url": "https://www.youtube.com/channel/UCoM_XmK45j504hfUWvN06Qg/live" 539 | } 540 | ] 541 | }, 542 | { 543 | "liver": "矢車りね", 544 | "room": [ 545 | { 546 | "site": "YouTube", 547 | "url": "https://www.youtube.com/channel/UCvzVB-EYuHFXHZrObB8a_Og/live" 548 | } 549 | ] 550 | } 551 | ] -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from datetime import datetime 4 | from time import sleep 5 | 6 | from src.liveScheduler import LiveScheduler 7 | from src.utitls import errmsg, logmsg, tracemsg 8 | from src.makeLive import makeLives, saveLives 9 | from src.rebroadcast import rebroadcast 10 | from src.Configs import CONFIGs 11 | from src.webSite import web 12 | 13 | 14 | def main(CONFIG_PATH): 15 | '''程序主入口 16 | 17 | 调用makeLives获得live列表,添加到scheduler后启动scheduler。 18 | 19 | Args: 20 | CONFIG_PATH: str, config.ini的储存位置。 21 | ''' 22 | # CONFIG初始化,必须放在所有步骤前 23 | configs = CONFIGs() 24 | configs.set_configs(CONFIG_PATH) 25 | WEB_PORT = configs.WEB_PORT 26 | 27 | logmsg('程序启动') 28 | 29 | # 解析schedule.txt并发送每日转播表动态 30 | # 发送每日动态的任务已整合入LiveScheduler类中 31 | lives = makeLives() 32 | # post_schedule(lives) 33 | 34 | # LiveScheduler为单例类,初始化需在web运行前 35 | scheduler = LiveScheduler(timezone='Asia/Tokyo') 36 | for live in lives: 37 | scheduler.add_live(rebroadcast, live) 38 | 39 | try: 40 | scheduler.start() 41 | logmsg('时间表启动') 42 | 43 | # APScheduler直接调用shutdown不会等待未开始执行的任务 44 | # get_jobs返回空列表时所有任务都已开始执行 45 | # 此时调用shutdown才会等待已开始执行的任务结束 46 | # while len(scheduler.get_jobs()) != 0: 47 | # sleep(600) 48 | 49 | web.run(host='0.0.0.0', port=WEB_PORT) 50 | 51 | scheduler.shutdown() 52 | except KeyboardInterrupt: 53 | errmsg('normal', '因KeyboardInterrupt退出') 54 | except Exception as e: 55 | msg = str(e) +'\n' + tracemsg(e) 56 | errmsg('normal', msg) 57 | 58 | if scheduler.running: 59 | scheduler.shutdown(wait=False) 60 | saveLives(scheduler.get_lives().values()) 61 | logmsg('程序结束') 62 | 63 | 64 | if __name__ == '__main__': 65 | main('config.ini') -------------------------------------------------------------------------------- /oldFile/bilibili.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import base64 4 | import json 5 | import re 6 | import sys 7 | import time 8 | 9 | import requests 10 | 11 | 12 | class Bilibili: 13 | def __init__(self): 14 | self.session = requests.session() 15 | self.csrf = None 16 | 17 | def login_by_cookies(self, path): 18 | try: 19 | with open(path, 'r') as f: 20 | cookies = {} 21 | for line in f.read().split(';'): 22 | name, value = line.strip().split('=', 1) 23 | cookies[name] = value 24 | cookies = requests.utils.cookiejar_from_dict(cookies, cookiejar=None, overwrite=True) 25 | self.session.cookies = cookies 26 | self.csrf = self.session.cookies.get('bili_jct') 27 | print("[提示]Cookies设置成功") 28 | except: 29 | print("[提示]设定cookies失败,请检查是否写入正确的cookies信息") 30 | 31 | def login_by_cookies_str(self, cookies_str): 32 | try: 33 | cookies = {} 34 | for line in cookies_str.split(';'): 35 | name, value = line.strip().split('=', 1) 36 | cookies[name] = value 37 | cookies = requests.utils.cookiejar_from_dict(cookies, cookiejar=None, overwrite=True) 38 | self.session.cookies = cookies 39 | self.csrf = self.session.cookies.get('bili_jct') 40 | print("Cookies设置成功") 41 | except Exception as e: 42 | print("[提示]设定cookies失败,请检查是否写入正确的cookies信息") 43 | 44 | def isLogin(self): 45 | req = self.get('https://api.vc.bilibili.com/feed/v1/feed/get_attention_list') 46 | code = req['code'] 47 | if code == 0: 48 | print("[提示]登录成功!") 49 | return True 50 | else: 51 | print("[提示]cookies失效!") 52 | print("[提示]登录返回信息为:" + req) 53 | sys.exit(1) 54 | def post(self, url, data, headers=None, params=None): 55 | while True: 56 | try: 57 | if headers is None: 58 | if params is None: 59 | req = self.session.post(url, data=data,timeout=99999) 60 | else: 61 | req = self.session.post(url, data=data, params=params,timeout=99999) 62 | else: 63 | if params is None: 64 | req = self.session.post(url, data=data, headers=headers,timeout=99999) 65 | else: 66 | req = self.session.post(url, data=data, headers=headers, params=params,timeout=99999) 67 | # print(req.url) 68 | if req.status_code == 200: 69 | try: 70 | return req.json() 71 | except Exception as e: 72 | print("[POST][提示]JSON化失败:"+str(e)+"\n[提示]内容为:"+req.text) 73 | return req.text 74 | else: 75 | print("[提示]状态码为"+str(req.status_code)+"!请检查错误\n[提示]" + req.text) 76 | sys.exit(0) 77 | except Exception as e: 78 | print("[提示]POST出错\n[提示]%s" % str(e)) 79 | 80 | def get(self, url, params=None, headers=None): 81 | while True: 82 | try: 83 | if params is None: 84 | if headers is None: 85 | req = self.session.get(url, timeout=5) 86 | else: 87 | req = self.session.get(url, headers=headers, timeout=5) 88 | else: 89 | if headers is None: 90 | req = self.session.get(url, params=params, timeout=5) 91 | else: 92 | req = self.session.get(url, params=params, headers=headers, timeout=5) 93 | if req.status_code == 200: 94 | try: 95 | # print(req.text) 96 | return req.json() 97 | except Exception as e: 98 | # print("[GET][提示]JSON化失败:" + str(e) + "\n[提示]内容为:" + req.text) 99 | return req.content.decode('utf-8') 100 | else: 101 | print("[提示]状态码为" + str(req.status_code) + "!请检查错误\n[提示]" + req.text) 102 | # sys.exit(0) 103 | time.sleep(1) 104 | except Exception as e: 105 | print("[提示]GET出错\n[提示]%s" % str(e)) 106 | 107 | def getMyChooseArea(self, mid): 108 | """ 109 | 查询我的直播间最近使用过的分类 110 | :param mid:直播间id 111 | :return: 112 | """ 113 | req = self.get( 114 | url='https://api.live.bilibili.com/room/v1/Area/getMyChooseArea', 115 | params={'roomid':mid} 116 | ) 117 | print(req) 118 | if req['code'] == 0: 119 | return req['data'] 120 | 121 | def getLiveAreaList(self, show_pinyin=1): 122 | """ 123 | 获得直播分类信息 124 | :param show_pinyin: 125 | :return: 126 | """ 127 | req = self.get( 128 | url='https://api.live.bilibili.com/room/v1/Area/getList', 129 | params={'show_pinyin': show_pinyin} 130 | ) 131 | print(req) 132 | 133 | def startLive(self, room_id, area_id): 134 | """ 135 | 开始直播,获取推流码 136 | :param room_id: 自己直播间id 137 | :param area_id: 直播间分区id 138 | :return: 139 | """ 140 | req = self.post( 141 | url='https://api.live.bilibili.com/room/v1/Room/startLive', 142 | data={ 143 | 'room_id': room_id, 144 | 'platform': 'pc', 145 | 'area_v2': area_id, 146 | 'csrf_token': self.csrf 147 | } 148 | ) 149 | if req['code'] == 0: 150 | rtmp_code = req['data']['rtmp']['addr']+req['data']['rtmp']['code'] 151 | new_link = req['data']['rtmp']['new_link'] 152 | # print("[提示]开播成功,获得推流地址:{}".format(rtmp_code)) 153 | # print("[提示]开播成功,获得new_link:{}".format(new_link)) 154 | return rtmp_code 155 | else: 156 | print("[提示]开播出现问题!code={},message={}".format(req['code'],req['message'])) 157 | 158 | def stopLive(self, room_id): 159 | """ 160 | 关闭我的直播 161 | :param room_id: 直播间id 162 | :return: 163 | """ 164 | req = self.post( 165 | url='https://api.live.bilibili.com/room/v1/Room/stopLive', 166 | data={ 167 | 'room_id': room_id, 168 | 'platform': 'pc', 169 | 'csrf_token': self.csrf 170 | } 171 | ) 172 | if req['code'] == 0 and req['message'] == '' and req['data']['change'] == 1: 173 | print("[提示]关播成功!") 174 | elif req['code'] == 0 and req['message'] == '重复关播' and req['data']['change'] == 0: 175 | print("[提示]重复关播!请勿重复提交关播请求!") 176 | else: 177 | print(req) 178 | 179 | def getMyRoomId(self): 180 | req = self.get( 181 | url='https://api.live.bilibili.com/i/api/liveinfo' 182 | ) 183 | # print(req) 184 | if req['code'] == 0: 185 | print("[提示]获得直播间id为[{}]".format(req['data']['roomid'])) 186 | return req['data']['roomid'] 187 | else: 188 | print("[提示]无法获得直播间id") 189 | 190 | def updateRoomTitle(self, room_id, title): 191 | req = self.post( 192 | url='https://api.live.bilibili.com/room/v1/Room/update', 193 | data={ 194 | 'room_id': room_id, 195 | 'title': title, 196 | 'csrf_token': self.csrf 197 | } 198 | ) 199 | if req['code'] == 0: 200 | print("[提示]直播间{}标题已更新为:{}".format(room_id, title)) 201 | else: 202 | print("[提示]修改失败!返回信息为:{}".format(req)) 203 | 204 | def get_my_basic_info(self): 205 | """ 206 | 获得此账号的基本信息(个人中心-我的信息) 207 | :return: 208 | """ 209 | req = self.get( 210 | url='https://api.bilibili.com/x/member/web/account', 211 | 212 | ) 213 | if req['code'] == 0: 214 | return req['data'] -------------------------------------------------------------------------------- /oldFile/utils.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import requests 4 | 5 | """ 6 | 说明 7 | (1)channel_id 频道名称类似于链接https://www.twitch.tv/shroud,shroud即为频道名称 8 | (2)vod,直播录像id,类似于链接https://www.twitch.tv/videos/303763956,303763956即为录像id 9 | (3)get_channel_live_m3u8默认返回chunked画质(即为原画质)的m3u8链接 10 | """ 11 | 12 | 13 | def get_channel_live_m3u8(channel_id): 14 | """ 15 | 获得频道直播的m3u8 16 | :param channel_id: 17 | :return: 18 | """ 19 | client_id = '4zswqk0crwt2wy4b76aaltk2z02m67' 20 | print("[提示]当前使用CLIENT_ID:{}".format(client_id)) 21 | req = requests.get( 22 | url='https://api.twitch.tv/api/channels/{}/access_token'.format(channel_id), 23 | params={'client_id': client_id} 24 | 25 | ) 26 | # print(req) 27 | sig = req.json()['sig'] 28 | token = req.json()['token'] 29 | print("[提示]获得signature:{},获得token:{}".format(token, sig)) 30 | req = requests.get(url='https://usher.ttvnw.net/api/channel/hls/{}.m3u8'.format(channel_id), 31 | params={ 32 | 'allow_source': 'true', 33 | 'nauth': token, 34 | 'nauthsig': sig, 35 | }) 36 | print(req.text) 37 | videolist = [] 38 | besturl = '' 39 | lines = req.text.split('\n') 40 | for i in range(2, len(lines)-1, 3): 41 | info = re.findall('BANDWIDTH=(\\d+),RESOLUTION=(.*?),CODECS="(.*?)",VIDEO="(.*?)"', lines[i+1])[0] 42 | # print(lines[i+1]) 43 | if info[3] == 'chunked': 44 | besturl = lines[i+2] 45 | videolist.append({ 46 | "best": lines[i+2] 47 | }) 48 | videolist.append({ 49 | 'bandwidth': info[0], 50 | 'codecs': info[2], 51 | 'resolution': info[1], 52 | 'video': info[3], 53 | 'url': lines[i+2] 54 | }) 55 | return besturl 56 | 57 | 58 | def get_vod_m3u8(vod): 59 | """ 60 | 获得直播录像的m3u8 61 | :param vod: 62 | :return: 63 | """ 64 | client_id = '4zswqk0crwt2wy4b76aaltk2z02m67' 65 | print("[提示]使用CLIENT_ID:{}解析VOD:{}".format(client_id, vod)) 66 | req = requests.get( 67 | url='https://api.twitch.tv/api/vods/{}/access_token'.format(vod), 68 | params={'client_id': client_id} 69 | ) 70 | sig = req.json()['sig'] 71 | token = req.json()['token'] 72 | print("[提示]获得signature:{},获得token:{}".format(token, sig)) 73 | print("[提示]获取VOD:{}的m3u8地址...".format(vod)) 74 | req = requests.get( 75 | url='https://usher.ttvnw.net/vod/{}.m3u8'.format(vod), 76 | params={ 77 | 'allow_source': 'true', 78 | 'nauth': token, 79 | 'nauthsig': sig, 80 | }) 81 | videolist = [] 82 | besturl = '' 83 | lines = req.text.split('\n') 84 | for i in range(2, len(lines)-1, 3): 85 | info = re.findall('BANDWIDTH=(\\d+),CODECS="(.*?)",RESOLUTION="(.*?)",VIDEO="(.*?)"', lines[i+1])[0] 86 | if info[3] == 'chunked': 87 | besturl = lines[i+2] 88 | videolist.append({ 89 | "best": lines[i+2] 90 | }) 91 | videolist.append({ 92 | 'bandwidth': info[0], 93 | 'codecs': info[1], 94 | 'resolution': info[2], 95 | 'video': info[3], 96 | 'url': lines[i+2] 97 | }) 98 | return besturl -------------------------------------------------------------------------------- /oldFile/youtube.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import configparser 4 | import os 5 | import re 6 | import time 7 | from datetime import datetime 8 | from bilibili import Bilibili 9 | # 读取配置文件 10 | config = configparser.ConfigParser() 11 | config.read("config.ini", encoding="utf-8") 12 | COOKIES_TXT_PATH = config.get('basic', 'COOKIES_TXT_PATH') 13 | BILIBILI_ROOM_TITLE = config.get('live', 'BILIBILI_ROOM_TITLE') 14 | FFMPEG_COMMAND = config.get('live', 'FFMPEG_COMMAND') 15 | ALWAYS_USE_HIGHEST_QUALITY = config.getboolean('youtube-dl', 'ALWAYS_USE_HIGHEST_QUALITY') 16 | BILIBILI_ROOM_AREA_ID = config.getint('live', 'BILIBILI_ROOM_AREA_ID') 17 | 18 | 19 | if __name__ == '__main__': 20 | LOGIN_STATUS = False 21 | print("[提示]Youtube转播工具v1.0") 22 | b = Bilibili() 23 | if os.path.exists(COOKIES_TXT_PATH): # 自动登录 24 | print("[提示]找到cookies.txt,尝试自动登录...") 25 | b.login_by_cookies(COOKIES_TXT_PATH) 26 | if b.isLogin(): 27 | LOGIN_STATUS = True 28 | if not LOGIN_STATUS: 29 | cookies = input("[提示]请粘贴cookies信息:") 30 | b.login_by_cookies_str(cookies) 31 | b.isLogin() 32 | my_info = b.get_my_basic_info() 33 | print("[提示][已登录账号{}][mid:{}][昵称:{}]".format(my_info['userid'], my_info['mid'], my_info['uname'])) 34 | print("[提示]请输入Youtube直播地址:") 35 | print("[提示]格式1.https://www.youtube.com/watch?v=xXxxXxxxXxx") 36 | print("[提示]格式2.https://www.youtube.com/channel/UCxxxxXXxXXXXXXxxxxXXXx/live") 37 | print("[提示]格式3.https://youtu.be/XxxxXXXxxxX") 38 | youtube_live_url = input("请输入:") 39 | f = os.popen('youtube-dl -F {} --no-check-certificate'.format(youtube_live_url)) 40 | codes = [] 41 | ana = f.read().strip() 42 | print(ana) 43 | for line in ana.split('\n'): 44 | code = re.findall('(\\d+)', line[:5]) 45 | if len(code) != 0: 46 | codes.append(int(code[0])) 47 | codes.sort() 48 | if not ALWAYS_USE_HIGHEST_QUALITY: # 自动选择清晰度 49 | quality_code = int(input("[提示]请选择推流清晰度,只可在{}中选择:\n".format(codes))) 50 | while True: 51 | if quality_code not in codes: 52 | print("[提示]请输入正确的清晰度代码") 53 | quality_code = int(input("[提示]请选择推流清晰度,只可在{}中选择:\n".format(codes))) 54 | else: 55 | break 56 | print("[提示]获得指定清晰度的m3u8地址...") 57 | else: 58 | quality_code = codes[-1] 59 | print("[提示]自动获得最高清晰度的m3u8地址...") 60 | while True: 61 | try: 62 | f = os.popen('youtube-dl -f {} -g {} --no-check-certificate'.format(quality_code, youtube_live_url)) 63 | m3u8_url = f.read().strip() 64 | print(len(m3u8_url)) 65 | print("[提示]获得直播源m3u8地址:" + m3u8_url) 66 | room_id = b.getMyRoomId() 67 | b.updateRoomTitle(room_id, BILIBILI_ROOM_TITLE) 68 | rtmp_code = b.startLive(room_id, BILIBILI_ROOM_AREA_ID) 69 | print("[提示]开播成功,获得推流地址:{}".format(rtmp_code)) 70 | time.sleep(5) 71 | command = FFMPEG_COMMAND.format(m3u8_url, rtmp_code) 72 | os.system(command) 73 | except Exception as e: 74 | with open("log.txt", "a") as log: 75 | log.write("{}发生错误,错误信息:{}".format(datetime.now(), e)) 76 | 77 | 78 | -------------------------------------------------------------------------------- /refresh_area_id.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''refresh_area_id.py 3 | 登陆Bilibili,读取直播分区信息,并更新area_id.txt。 4 | 需要Bilibili类,并cookies.txt中已填好可登陆的cookie。 5 | 代码未封装到函数中,不建议在其他代码中导入。 6 | ''' 7 | 8 | from datetime import datetime 9 | 10 | from src.methods.m_bilibili import Bilibili 11 | 12 | 13 | txt = '最终修改于:{time}\n\n'.format(time=datetime.now().strftime(r'%Y %m.%d %H:%M')) 14 | b = Bilibili() 15 | b.login_by_cookies('cookies.txt') 16 | if b.isLogin(): 17 | getLiveAreaListReq=b.get( 18 | url='https://api.live.bilibili.com/room/v1/Area/getList', 19 | params={'show_pinyin': 1} 20 | )['data'] 21 | getLiveAreaListReq.sort(key=lambda zone:zone['id']) 22 | for zone in getLiveAreaListReq: 23 | txt += '{id}.{name}\n'.format(id=zone['id'], name=zone['name']) 24 | zone['list'].sort(key=lambda area:area['id']) 25 | for area in zone['list']: 26 | if area['lock_status'] == '0': 27 | txt += ' {id}.{name}\n'.format(id=area['id'], name=area['name']) 28 | 29 | print(txt) 30 | with open('area_id.txt','w', encoding='utf-8') as f: 31 | f.write(txt) 32 | -------------------------------------------------------------------------------- /schedule.txt: -------------------------------------------------------------------------------- 1 | # 时间表 2 | # 格式: 3 | # time@liver@site@title 4 | # time:直播时间,可填入"now",会自动认为在读取时间表30秒后开播。 5 | # 此外只能填入HHMM四位数字。 6 | # 只接受未来24小时内的直播,检测出直播时间在运行程序之前时,会自动认为直播在运行程序第二天开始。 7 | # liver:只接受liveInfo.json中存在的liver名。(可自行按json格式添加到liveInfo文件中) 8 | # site:直播网站,目前只接受YouTube。而且不填默认为YouTube。 9 | # title:填入config.ini中直播间标题的title中,可选,不填默认为"{liver}"+"转播" 10 | 11 | # 例: 12 | # 1900@桜凛月@绝地求生 13 | # 2100@黒井しば 14 | # 2240@飛鳥ひな@YouTube 15 | # 0640@伏見ガク@YouTube@おはガク! -------------------------------------------------------------------------------- /src/Configs.py: -------------------------------------------------------------------------------- 1 | from configparser import ConfigParser 2 | 3 | from src.utitls import sigleton 4 | 5 | 6 | @sigleton 7 | class CONFIGs: 8 | def set_configs(self, path): 9 | '''CONFIG初始化 10 | 11 | 读取路径为path的设定文件 12 | 必须在所有使用到设定的步骤之前 13 | ''' 14 | config = ConfigParser() 15 | config.read(path, encoding='utf-8') 16 | self.__CONFIG_PATH = path 17 | 18 | # 基本设置,不会在设置页面显示 19 | self.WEB_PORT = config.getint('basic', 'WEB_PORT') 20 | self.COOKIES_TXT_PATH = config.get('basic', 'COOKIES_TXT_PATH') 21 | self.LIVE_INFO_PATH = config.get('basic', 'LIVE_INFO_PATH') 22 | self.SCHEDULE_TXT_PATH = config.get('basic', 'SCHEDULE_TXT_PATH') 23 | self.FFMPEG_COMMAND = config.get('basic', 'FFMPEG_COMMAND') 24 | 25 | # bilibili相关设置 26 | self.BILIBILI_ROOM_TITLE = config.get('bilibili', 'BILIBILI_ROOM_TITLE') 27 | self.DEFAULT_TITLE_PARAM = config.get('bilibili', 'DEFAULT_TITLE_PARAM') 28 | self.BILIBILI_ROOM_AREA_ID = config.getint('bilibili', 'BILIBILI_ROOM_AREA_ID') 29 | self.IS_SEND_DAILY_DYNAMIC = config.getboolean('bilibili', 'IS_SEND_DAILY_DYNAMIC') 30 | self.DAILY_DYNAMIC_FORM = config.get('bilibili', 'DAILY_DYNAMIC_FORM') 31 | self.IS_SEND_PRELIVE_DYNAMIC = config.getboolean('bilibili', 'IS_SEND_PRELIVE_DYNAMIC') 32 | self.PRELIVE_DYNAMIC_FORM = config.get('bilibili', 'PRELIVE_DYNAMIC_FORM') 33 | 34 | # 直播参数相关设置 35 | self.LIVE_QUALITY = config.getint('liveParam', 'LIVE_QUALITY') 36 | 37 | def save_configs(self): 38 | '''将实例中设置项成员保存至文件 39 | 40 | 更改设置项后应立即运行此函数 41 | 以免程序停止后设置项复原 42 | ''' 43 | config = ConfigParser() 44 | config.read(self.__CONFIG_PATH, encoding='utf-8') 45 | 46 | # 基本设置不能在程序中更改 47 | 48 | # bilibili相关设置 49 | config.set('bilibili', 'BILIBILI_ROOM_TITLE', str(self.BILIBILI_ROOM_TITLE)) 50 | config.set('bilibili', 'DEFAULT_TITLE_PARAM', str(self.DEFAULT_TITLE_PARAM)) 51 | config.set('bilibili', 'BILIBILI_ROOM_AREA_ID', str(self.BILIBILI_ROOM_AREA_ID)) 52 | config.set('bilibili', 'IS_SEND_DAILY_DYNAMIC', str(self.IS_SEND_DAILY_DYNAMIC)) 53 | config.set('bilibili', 'DAILY_DYNAMIC_FORM', str(self.DAILY_DYNAMIC_FORM)) 54 | config.set('bilibili', 'IS_SEND_PRELIVE_DYNAMIC', str(self.IS_SEND_PRELIVE_DYNAMIC)) 55 | config.set('bilibili', 'PRELIVE_DYNAMIC_FORM', str(self.PRELIVE_DYNAMIC_FORM)) 56 | 57 | # 直播参数相关设置 58 | config.set('liveParam', 'LIVE_QUALITY', str(self.LIVE_QUALITY)) 59 | 60 | config.write(open(self.__CONFIG_PATH, 'w', encoding='utf-8')) -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyoJerryYu/autoLive/800d4e7d1e4a1843bd5594d3323ecd78bb1fff3f/src/__init__.py -------------------------------------------------------------------------------- /src/liveScheduler.py: -------------------------------------------------------------------------------- 1 | from apscheduler.schedulers.background import BackgroundScheduler 2 | from functools import wraps 3 | from datetime import datetime, timedelta, timezone 4 | 5 | from src.utitls import sigleton, errmsg, tracemsg, logmsg 6 | from src.Configs import CONFIGs 7 | from src.login_bilibili import login_bilibili 8 | 9 | 10 | @sigleton 11 | class LiveScheduler(BackgroundScheduler): 12 | '''直播定时任务 13 | 14 | 单例类,保证不同模块中可使用同一个实例 15 | 16 | Members: 17 | __scheduler: BackgroundScheduler 18 | __lives: dict, key为live.id(), value为对应的live类 19 | __livings: dict, key与value同上,保存正在进行中的live类 20 | 21 | Methods: 22 | add_live, get_lives, get_live, remove_live, post_schedule 23 | ''' 24 | def __init__(self, *args, **kw): 25 | '''构造函数 26 | 27 | 构造实例时会添加自动发送每日动态的任务 28 | 每日本地时间15时时发送每日动态 29 | 由于是单例类,所以此任务不会重复添加 30 | ''' 31 | self.__lives = {} 32 | self.__livings = {} 33 | super().__init__(*args, **kw) 34 | 35 | # 此处post_schedule使用本地时区 36 | # APScheduler中使用时区只支持pytz 37 | self.add_job( 38 | self.post_schedule, 39 | trigger='cron', 40 | hour=15 41 | ) 42 | 43 | def __exefunc_decorator(self, func): 44 | '''执行函数装饰器 45 | 46 | 用于装饰live执行函数 47 | 使调用前将live移出__lives并加入__livings 48 | 调用后移出__livings 49 | 在add_live内部调用 50 | 不改变原函数定义 51 | 52 | :param func: live执行函数, 参数应为(live, *args) 53 | :return wrapper: 54 | ''' 55 | @wraps(func) 56 | def wrapper(live, *args): 57 | live_id = live.live_id() 58 | self.__lives.pop(live_id) 59 | self.__livings[live_id] = { 60 | 'live': live, 61 | 'startT': datetime.now() 62 | } 63 | func(live, *args) 64 | self.__livings.pop(live_id) 65 | return wrapper 66 | 67 | def add_live(self, func, live, *args): 68 | '''添加live项目进入时间表 69 | 70 | 把live用对应格式加入scheduler 71 | 并加入live列表后返回对应live的live_id 72 | 73 | live_id是时间表中live的唯一标记 74 | 可用于pop_live与get_live函数 75 | 76 | :param func: 执行转播的函数 77 | :param live: live类,直播项目 78 | :return live_id: 时间表中对应live实例的live_id 79 | ''' 80 | live_id = live.live_id() 81 | self.add_job( 82 | func=self.__exefunc_decorator(func), 83 | trigger='date', 84 | run_date=live.time, 85 | args=[live]+[x for x in args], 86 | id=live_id 87 | ) 88 | self.__lives[live_id] = live 89 | return live_id 90 | 91 | def get_lives(self): 92 | '''获取时间表中的live列表 93 | ''' 94 | return self.__lives 95 | 96 | def get_live(self, live_id): 97 | '''获取live_id对应的live实例 98 | ''' 99 | return self.__lives[live_id] 100 | 101 | def pop_live(self, live_id): 102 | '''将live_id对应live移出时间表并返回live实例 103 | ''' 104 | self.remove_job(live_id) 105 | live = self.__lives.pop(live_id) 106 | return live 107 | 108 | def get_livings(self): 109 | '''获取已运行的live列表 110 | ''' 111 | return self.__livings 112 | 113 | def __make_schedule_post_txt(self, lives): 114 | '''读取lives列表,输出用于发动态的时间表字符串 115 | ''' 116 | DAILY_DYNAMIC_FORM = CONFIGs().DAILY_DYNAMIC_FORM 117 | txt = '今日转播:\n时间均为日本时区\n' 118 | for live in lives: 119 | txt += DAILY_DYNAMIC_FORM.format( 120 | time=live.time.strftime(r'%m.%d %H:%M'), 121 | liver=live.liver, 122 | site=live.site, 123 | title=live.title 124 | ) 125 | return txt 126 | 127 | def post_schedule(self, lives=None): 128 | '''发送时间表动态 129 | 130 | Args: 131 | lives: 直播信息列表 132 | 133 | Returns: 134 | 0: 正常发送动态 135 | -1: 发送动态过程中出错 136 | ''' 137 | # 兼容旧版保留lives参数 138 | # 如果没有给lives参数则获取时间表中的lives 139 | if not lives: 140 | lives = self.__lives.values() 141 | # Read config 142 | config = CONFIGs() 143 | COOKIES_TXT_PATH = config.COOKIES_TXT_PATH 144 | IS_SEND_DAILY_DYNAMIC = config.IS_SEND_DAILY_DYNAMIC 145 | 146 | # 如果没有直播预定或设置为不发送动态,则中止 147 | if len(lives) == 0 or not IS_SEND_DAILY_DYNAMIC: 148 | return 0 149 | 150 | # Post dynamic 151 | try: 152 | schedule_post_txt = self.__make_schedule_post_txt(lives) 153 | b = login_bilibili(COOKIES_TXT_PATH) 154 | if b.send_dynamic(schedule_post_txt): 155 | logmsg('发送每日动态成功') 156 | else: 157 | errmsg('发送每日动态失败') 158 | return -1 159 | except Exception as e: 160 | txt = '' 161 | if len(str(e).strip()) == 0: 162 | txt = '\n'+tracemsg(e) 163 | errmsg('schedule', str(e)+txt) 164 | return -1 165 | return 0 166 | 167 | 168 | class Live: 169 | '''直播信息 170 | 171 | Members: 172 | time, datetime.datetime, 直播开始时间 173 | liver, str, 直播liver名,应在liveInfo.json中存在 174 | site, str, 目前只支持YouTube 175 | title, str, 用于直播间标题的可变内容 176 | ''' 177 | def __init__(self, time, liver, site='', title=''): 178 | '''Live类构造函数 179 | 180 | site与title参数为与旧格式兼容 181 | 可支持省略或填入空字符串 182 | 183 | :param datetime|str time:可使用datetime实例或长度为4的时间字符串 184 | :param str liver: 185 | :param str site: 默认值为'YouTube' 186 | :param str title: 默认值为liver+' 转播' 187 | ''' 188 | if isinstance(time, str): 189 | time = Live.analyse_time_text(time) 190 | if site == '': 191 | site = 'YouTube' 192 | if title == '': 193 | DEFAULT_TITLE_PARAM = CONFIGs().DEFAULT_TITLE_PARAM 194 | title = DEFAULT_TITLE_PARAM.format( 195 | time=time.strftime(r'%m.%d %H:%M'), 196 | liver=liver, 197 | site=site 198 | ) 199 | self.time = time 200 | self.liver = liver 201 | self.site = site 202 | self.title = title 203 | 204 | def args(self): 205 | '''返回rebroadcast的args参数 206 | ''' 207 | args = { 208 | 'time': self.time, 209 | 'liver': self.liver, 210 | 'site': self.site, 211 | 'title': self.title 212 | } 213 | return args 214 | 215 | def live_id(self): 216 | '''返回用于scheduler的job_id 217 | ''' 218 | live_id = self.time.strftime('%H%M') + self.liver 219 | return live_id 220 | 221 | @staticmethod 222 | def analyse_time_text(time_txt): 223 | '''分析四位长度的time_txt并返回符合的datetime实例 224 | 225 | time_txt可接受特殊值:"now" 226 | 此外仅能填入长度为4位,格式为HHMM,的24小时制时间 227 | 当得出时间小于当前时,自动调整为第二天 228 | 229 | 特殊值: 230 | now: 返回datetime.now()的30秒后 231 | 232 | :param str time_txt: 四位长度HHMM字符串 233 | ''' 234 | JST = timezone(timedelta(hours=+9), 'JST') 235 | assert isinstance(time_txt, str) 236 | 237 | # time_txt填入now参数时 238 | if time_txt.lower() == 'now': 239 | time = datetime.now(JST) + timedelta(seconds=+30) 240 | return time 241 | 242 | # time格式仅能为HHMM,24小时制 243 | if len(time_txt) != 4: 244 | raise Exception(time_txt+'\n时间长度不正确') 245 | try: 246 | h = int(time_txt[0:-2]) 247 | m = int(time_txt[2:]) 248 | except Exception as e: 249 | raise Exception(time_txt+'\n无法转换为整数\n'+str(e)) 250 | if h < 0 or h > 24 or m < 0 or m > 60: 251 | raise Exception(time_txt+'\n时间不在可用范围内') 252 | 253 | # 识别24小时内的第二天 254 | now = datetime.now(JST) 255 | time = datetime(now.year, now.month, now.day, h, m,tzinfo=JST) 256 | if time < now: 257 | time += timedelta(days=+1) 258 | return time 259 | 260 | @staticmethod 261 | def livefScheduleTxt(line): 262 | '''读取一行时间表返回一个live实例 263 | 264 | 时间表格式: 265 | time@liver@site@title 266 | time: 直播时间,可接收特殊参数"now"。 267 | 此外格式仅能为HHMM,只接受未来24小时内的直播,检测出直播时间在运行程序之前时,会自动认为直播在运行程序第二天开始。 268 | liver:只接受liveInfo.json中存在的liver名。(可自行按json格式添加到liveInfo文件中) 269 | site:直播网站,目前只接受YouTube。而且不填默认为YouTube。 270 | title:填入config.ini中直播间标题的title中,可选,不填默认为"{liver}"+"转播" 271 | 272 | :param str line: 符合格式的时间表中的一行 273 | ''' 274 | sites = ['YouTube'] 275 | 276 | args = line.split('@') 277 | if len(args) < 2: 278 | raise Exception(line + '\n参数不足') 279 | time_txt, liver = args[0], args[1] 280 | 281 | # site 与 title 可省略 282 | site, title = '', '' 283 | if len(args) > 2: 284 | if args[2] in sites: 285 | site = args[2] 286 | if len(args) > 3: 287 | title = args[-1] 288 | else: 289 | title = args[-1] 290 | 291 | time = Live.analyse_time_text(time_txt) 292 | return Live(time, liver, site, title) -------------------------------------------------------------------------------- /src/login_bilibili.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from src.methods.m_bilibili import Bilibili 5 | from src.utitls import logmsg, errmsg 6 | 7 | 8 | def login_bilibili(path): 9 | '''封装登陆Bilibili时的log及raise exception 10 | 11 | Args: 12 | path: str, cookies文件路径 13 | 14 | Return: 15 | b: Bilibili类, 且已登录 16 | 17 | Raise: 18 | Exception: Cookies登陆失败 19 | ''' 20 | b = Bilibili() 21 | logmsg('尝试通过Cookies登陆Bilibili') 22 | LOGIN_STATUS = False 23 | if os.path.exists(path): 24 | b.login_by_cookies(path) 25 | if b.isLogin(): 26 | LOGIN_STATUS = True 27 | else: 28 | errmsg('login', '找不到cookies.txt') 29 | if LOGIN_STATUS == False: 30 | raise Exception('Cookies登陆Bilibili失败') 31 | my_info = b.get_my_basic_info() 32 | logmsg("[已登录账号{}][mid:{}][昵称:{}]".format(my_info['userid'], my_info['mid'], my_info['uname'])) 33 | return b -------------------------------------------------------------------------------- /src/makeLive.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''直播信息相关函数 3 | 一次直播信息记录在live类中 4 | ''' 5 | import os 6 | from datetime import datetime, timedelta, timezone 7 | 8 | from src.utitls import errmsg, tracemsg, logmsg 9 | from src.liveScheduler import Live 10 | from src.Configs import CONFIGs 11 | 12 | 13 | def __analyse_live_list(text): 14 | '''分析时间表text并输出直播信息列表 15 | 16 | 时间表text每行为 time@liver@site@title 17 | site可省略,可用值只能在sites中选择,默认为YouTube 18 | title可省略,默认为 liver+' 转播',如果参数过多不报错,title默认为最后一个参数 19 | 20 | Args: 21 | text: str, 22 | 23 | Returns: 24 | lives: list, live类列表 25 | ''' 26 | lives = [] 27 | for line in text.split('\n'): 28 | try: 29 | line = line.strip() 30 | # 跳过注释 31 | if len(line) == 0 or line[0] == '#': 32 | continue 33 | live = Live.livefScheduleTxt(line) 34 | lives.append(live) 35 | except Exception as e: 36 | txt = '' 37 | if len(str(e).strip()) == 0: 38 | txt = '\n'+tracemsg(e) 39 | errmsg('schedule', str(e)+txt) 40 | return lives 41 | 42 | 43 | def makeLives(): 44 | '''读取schedule.txt,解析,并输出直播信息列表 45 | 46 | Returns: 47 | lives: live类列表 48 | ''' 49 | # Read config 50 | config = CONFIGs() 51 | SCHEDULE_TXT_PATH = config.SCHEDULE_TXT_PATH 52 | 53 | # Get live list 54 | text = "" 55 | with open(SCHEDULE_TXT_PATH, encoding='utf-8') as file: 56 | text = file.read() 57 | lives = __analyse_live_list(text) 58 | lives.sort(key=lambda live:live.time) 59 | 60 | return lives 61 | 62 | 63 | def saveLives(lives): 64 | '''读取live列表,以相应格式输入schedule.txt 65 | ''' 66 | # Read config 67 | config = CONFIGs() 68 | SCHEDULE_TXT_PATH = config.SCHEDULE_TXT_PATH 69 | 70 | # Make text 71 | texts = [ 72 | '# 时间表', 73 | '# 格式:', 74 | '# time@liver@site@title', 75 | '# time:直播时间,格式为HHMM,只接受未来24小时内的直播,检测出直播时间在运行程序之前时,会自动认为直播在运行程序第二天开始。', 76 | '# liver:只接受liveInfo.json中存在的liver名。(可自行按json格式添加到liveInfo文件中)', 77 | '# site:直播网站,目前只接受YouTube。而且不填默认为YouTube。', 78 | '# title:填入config.ini中直播间标题的title中,可选,不填默认为liver+"转播"', 79 | '', 80 | '# 例:', 81 | '# 1900@桜凛月@绝地求生', 82 | '# 2100@黒井しば', 83 | '# 2240@飛鳥ひな@YouTube', 84 | '# 0640@伏見ガク@YouTube@おはガク!', 85 | ] 86 | for live in lives: 87 | line = '{time}@{liver}@{site}@{title}'.format( 88 | time=live.time.strftime('%H%M'), 89 | liver=live.liver, 90 | site=live.site, 91 | title=live.title 92 | ) 93 | texts.append(line) 94 | 95 | text = '' 96 | for line in texts: 97 | text += line+'\n' 98 | 99 | with open(SCHEDULE_TXT_PATH, 'w', encoding='utf-8') as file: 100 | file.write(text) -------------------------------------------------------------------------------- /src/methods/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyoJerryYu/autoLive/800d4e7d1e4a1843bd5594d3323ecd78bb1fff3f/src/methods/__init__.py -------------------------------------------------------------------------------- /src/methods/m_bilibili.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import base64 4 | import json 5 | import re 6 | import sys 7 | import time 8 | 9 | import requests 10 | 11 | 12 | class Bilibili: 13 | '''登陆、开始直播、发动态等与bilibili交互的类。 14 | 15 | 大部分未经修改引用自https://github.com/7rikka/autoLive 16 | 并添加了少部分函数 17 | 18 | Attributes: 19 | session: requests.session() 20 | csrf: B站账户验证时需的字符串,来自cookie中的bili-jct。 21 | ''' 22 | def __init__(self): 23 | self.session = requests.session() 24 | self.csrf = None 25 | 26 | def login_by_cookies(self, path): 27 | '''读取cookies文件并设置cookies 28 | 29 | Args: 30 | path: cookies.txt文件的路径 31 | ''' 32 | try: 33 | with open(path, 'r') as f: 34 | cookies = {} 35 | for line in f.read().split(';'): 36 | name, value = line.strip().split('=', 1) 37 | cookies[name] = value 38 | cookies = requests.utils.cookiejar_from_dict(cookies, cookiejar=None, overwrite=True) 39 | self.session.cookies = cookies 40 | self.csrf = self.session.cookies.get('bili_jct') 41 | print("[提示]Cookies设置成功") 42 | except: 43 | print("[提示]设定cookies失败,请检查是否写入正确的cookies信息") 44 | 45 | def login_by_cookies_str(self, cookies_str): 46 | '''直接通过cookies字符串登陆 47 | ''' 48 | try: 49 | cookies = {} 50 | for line in cookies_str.split(';'): 51 | name, value = line.strip().split('=', 1) 52 | cookies[name] = value 53 | cookies = requests.utils.cookiejar_from_dict(cookies, cookiejar=None, overwrite=True) 54 | self.session.cookies = cookies 55 | self.csrf = self.session.cookies.get('bili_jct') 56 | print("Cookies设置成功") 57 | except Exception as e: 58 | print("[提示]设定cookies失败,请检查是否写入正确的cookies信息") 59 | 60 | def isLogin(self): 61 | '''测试cookies能否登陆 62 | 63 | Returns: 64 | True | False 65 | ''' 66 | req = self.get('https://api.vc.bilibili.com/feed/v1/feed/get_attention_list') 67 | code = req['code'] 68 | if code == 0: 69 | print("[提示]登录成功!") 70 | return True 71 | else: 72 | print("[提示]cookies失效!") 73 | print("[提示]登录返回信息为:" + str(req)) 74 | return False 75 | 76 | def post(self, url, data, headers=None, params=None): 77 | while True: 78 | try: 79 | if headers is None: 80 | if params is None: 81 | req = self.session.post(url, data=data,timeout=99999) 82 | else: 83 | req = self.session.post(url, data=data, params=params,timeout=99999) 84 | else: 85 | if params is None: 86 | req = self.session.post(url, data=data, headers=headers,timeout=99999) 87 | else: 88 | req = self.session.post(url, data=data, headers=headers, params=params,timeout=99999) 89 | # print(req.url) 90 | if req.status_code == 200: 91 | try: 92 | return req.json() 93 | except Exception as e: 94 | print("[POST][提示]JSON化失败:"+str(e)+"\n[提示]内容为:"+req.text) 95 | return req.text 96 | else: 97 | print("[提示]状态码为"+str(req.status_code)+"!请检查错误\n[提示]" + req.text) 98 | sys.exit(0) 99 | except Exception as e: 100 | print("[提示]POST出错\n[提示]%s" % str(e)) 101 | 102 | def get(self, url, params=None, headers=None): 103 | while True: 104 | try: 105 | if params is None: 106 | if headers is None: 107 | req = self.session.get(url, timeout=5) 108 | else: 109 | req = self.session.get(url, headers=headers, timeout=5) 110 | else: 111 | if headers is None: 112 | req = self.session.get(url, params=params, timeout=5) 113 | else: 114 | req = self.session.get(url, params=params, headers=headers, timeout=5) 115 | if req.status_code == 200: 116 | try: 117 | # print(req.text) 118 | req.encoding = req.apparent_encoding 119 | return req.json() 120 | except Exception as e: 121 | # print("[GET][提示]JSON化失败:" + str(e) + "\n[提示]内容为:" + req.text) 122 | return req.content.decode('utf-8') 123 | else: 124 | print("[提示]状态码为" + str(req.status_code) + "!请检查错误\n[提示]" + req.text) 125 | # sys.exit(0) 126 | time.sleep(1) 127 | except Exception as e: 128 | print("[提示]GET出错\n[提示]%s" % str(e)) 129 | 130 | def getMyChooseArea(self, mid): 131 | """ 132 | 查询我的直播间最近使用过的分类 133 | :param mid:直播间id 134 | :return: 135 | """ 136 | req = self.get( 137 | url='https://api.live.bilibili.com/room/v1/Area/getMyChooseArea', 138 | params={'roomid':mid} 139 | ) 140 | print(req) 141 | if req['code'] == 0: 142 | return req['data'] 143 | 144 | def getLiveAreaList(self, show_pinyin=1): 145 | """ 146 | 获得直播分类信息 147 | :param show_pinyin: 148 | :return: 149 | """ 150 | req = self.get( 151 | url='https://api.live.bilibili.com/room/v1/Area/getList', 152 | params={'show_pinyin': show_pinyin} 153 | ) 154 | if req['code'] == 0: 155 | return req['data'] 156 | 157 | 158 | def startLive(self, room_id, area_id): 159 | """ 160 | 开始直播,获取推流码 161 | :param room_id: 自己直播间id 162 | :param area_id: 直播间分区id 163 | :return: 164 | """ 165 | req = self.post( 166 | url='https://api.live.bilibili.com/room/v1/Room/startLive', 167 | data={ 168 | 'room_id': room_id, 169 | 'platform': 'pc', 170 | 'area_v2': area_id, 171 | 'csrf_token': self.csrf 172 | } 173 | ) 174 | if req['code'] == 0: 175 | rtmp_code = req['data']['rtmp']['addr']+req['data']['rtmp']['code'] 176 | new_link = req['data']['rtmp']['new_link'] 177 | # print("[提示]开播成功,获得推流地址:{}".format(rtmp_code)) 178 | # print("[提示]开播成功,获得new_link:{}".format(new_link)) 179 | return rtmp_code 180 | else: 181 | print("[提示]开播出现问题!code={},message={}".format(req['code'],req['message'])) 182 | 183 | def stopLive(self, room_id): 184 | """ 185 | 关闭我的直播 186 | :param room_id: 直播间id 187 | :return: 188 | """ 189 | req = self.post( 190 | url='https://api.live.bilibili.com/room/v1/Room/stopLive', 191 | data={ 192 | 'room_id': room_id, 193 | 'platform': 'pc', 194 | 'csrf_token': self.csrf 195 | } 196 | ) 197 | if req['code'] == 0 and req['message'] == '' and req['data']['change'] == 1: 198 | print("[提示]关播成功!") 199 | elif req['code'] == 0 and req['message'] == '重复关播' and req['data']['change'] == 0: 200 | print("[提示]重复关播!请勿重复提交关播请求!") 201 | else: 202 | print(req) 203 | 204 | def getMyRoomId(self): 205 | req = self.get( 206 | url='https://api.live.bilibili.com/i/api/liveinfo' 207 | ) 208 | # print(req) 209 | if req['code'] == 0: 210 | print("[提示]获得直播间id为[{}]".format(req['data']['roomid'])) 211 | return req['data']['roomid'] 212 | else: 213 | print("[提示]无法获得直播间id") 214 | 215 | def getRoomTitle(self, room_id): 216 | req = self.get( 217 | url='https://api.live.bilibili.com/room/v1/Room/get_info', 218 | params={ 219 | 'room_id': room_id, 220 | 'from': 'room' 221 | } 222 | ) 223 | if req['code'] == 0: 224 | print("[提示]获得直播间标题为[{}]".format(req['data']['title'])) 225 | return req['data']['title'] 226 | 227 | def updateRoomTitle(self, room_id, title): 228 | req = self.post( 229 | url='https://api.live.bilibili.com/room/v1/Room/update', 230 | data={ 231 | 'room_id': room_id, 232 | 'title': title, 233 | 'csrf_token': self.csrf 234 | } 235 | ) 236 | if req['code'] == 0: 237 | print("[提示]直播间{}标题已更新为:{}".format(room_id, title)) 238 | else: 239 | print("[提示]修改失败!返回信息为:{}".format(req)) 240 | 241 | def get_my_basic_info(self): 242 | """ 243 | 获得此账号的基本信息(个人中心-我的信息) 244 | :return: 245 | """ 246 | req = self.get( 247 | url='https://api.bilibili.com/x/member/web/account', 248 | 249 | ) 250 | if req['code'] == 0: 251 | return req['data'] 252 | 253 | def send_dynamic(self, content): 254 | '''发送动态 255 | 256 | post访问http://api.vc.bilibili.com/dynamic_repost/v1/dynamic_repost/repost 257 | 发送Bilibili动态 258 | 259 | Args: 260 | content: str, 动态的内容 261 | 262 | Returns: 263 | req['data']['dynamic_id']: post请求返回的动态id。 264 | req['data']如下: 265 | 266 | { 267 | 'result': 0, 268 | 'errmsg': '符合条件,允许发布', 269 | 'dynamic_id': xxxxxxxxxxxx(int), 270 | 'create_result': 1, 271 | '_gt_': 0 272 | } 273 | ''' 274 | req = self.post( 275 | url='http://api.vc.bilibili.com/dynamic_repost/v1/dynamic_repost/repost', 276 | data={ 277 | 'dynamic_id': '0', 278 | 'type': '4', 279 | 'rid': '0', 280 | 'content': content.replace(r'\n', '\n'), 281 | 'at_uids': '', 282 | 'ctrl': '[]', 283 | 'csrf_token': self.csrf 284 | } 285 | ) 286 | if req['code'] == 0: 287 | return req['data']['dynamic_id'] 288 | 289 | def delete_dynamic(self, dynamic_id): 290 | '''删除动态 291 | 292 | :param int dynamic_id: 所删除动态的id 293 | ''' 294 | uid = self.get_my_basic_info()['mid'] 295 | req = self.post( 296 | url='https://api.vc.bilibili.com/dynamic_repost/v1/dynamic_repost/rm_rp_dyn', 297 | data={ 298 | 'uid': uid, 299 | 'dynamic_id': dynamic_id, 300 | 'csrf_token': self.csrf 301 | } 302 | ) 303 | if req['code'] == 0: 304 | return req['data'] -------------------------------------------------------------------------------- /src/methods/m_youtube.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | import re 4 | from src.utitls import logmsg, errmsg, RunCMD 5 | import json 6 | 7 | 8 | def get_m3u8(url_live, live_quality): 9 | '''获得YouTube直播的m3u8地址 10 | 11 | 调用youtube-dl,获得清晰度列表, 12 | 并返回其中清晰度不高于live_quality的最高清晰度m3u8。 13 | 14 | Args: 15 | url_live: str, 直播间的url 16 | live_quality: int, 最高清晰度,返回的m3u8清晰度不会高于此值 17 | 18 | Returns: 19 | m3u8_url: str, 直播的m3u8地址 20 | 21 | Raise: 22 | Exception 23 | ''' 24 | # 获取清晰度 25 | out, err, errcode = RunCMD('youtube-dl --no-check-certificate -j {}'.format(url_live)) 26 | out = out.decode('utf-8') if isinstance(out, (bytes, bytearray)) else out 27 | if errcode != 0: 28 | raise Exception('youtube-dl不正常返回,code={}'.format(errcode)) 29 | 30 | try: 31 | vDict = json.loads(out) 32 | except Exception: 33 | raise Exception('清晰度列表无法用json解析') 34 | 35 | try: 36 | # 按清晰度由小到大排序 37 | vDict['formats'].sort(key=lambda live_format : live_format['height']) 38 | 39 | count = -1 40 | if live_quality != 0: 41 | for live_format in vDict['formats']: 42 | if live_format['height'] <= live_quality: 43 | count += 1 # 指向不大于所选择清晰度的最大值 44 | else: 45 | break 46 | if count == -1: 47 | count = 0 # 选择清晰度小于最小清晰度时,返回最小清晰度 48 | else: 49 | count = -1 # 自动使用最高清晰度时,返回最后一组live_format 50 | 51 | # 获取m3u8 52 | m3u8_url = vDict['formats'][count]['url'] 53 | logmsg('获得直播源m3u8地址:\n{m3u8}'.format(m3u8=m3u8_url)) 54 | return m3u8_url 55 | except Exception: 56 | raise Exception('解析清晰度时格式出错,vDict:\n{}'.format(json.dumps(vDict, ensure_ascii=False, indent=2))) 57 | 58 | 59 | def push_stream(url_rtmp, url_live, url_m3u8, command): 60 | '''调用ffmpeg将url_rtmp推至url_rtmp 61 | 62 | Args: 63 | url_rtmp: 64 | url_live: 仅用于记录 65 | url_m3u8: 66 | command: str, 推流命令 67 | ''' 68 | logmsg('开始推流\nusing push_stream in Youtube\n{}\n{}\n{}\n{}\n'.format(url_rtmp,url_live,url_m3u8,command)) 69 | command = command.format(url_m3u8, url_rtmp) 70 | out, err, errcode = RunCMD(command) 71 | logmsg('结束推流') 72 | return out, err, errcode 73 | 74 | -------------------------------------------------------------------------------- /src/rebroadcast.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | from time import sleep 4 | 5 | from src.methods.m_bilibili import Bilibili 6 | from src.methods import m_youtube 7 | from src.login_bilibili import login_bilibili 8 | from src.utitls import logmsg, errmsg, loadJson, tracemsg 9 | from src.Configs import CONFIGs 10 | 11 | 12 | def get_live_url(path, liver, site='YouTube'): 13 | lives_info = loadJson(path) 14 | live_url = '' 15 | for live_info in lives_info: 16 | if live_info['liver'] == liver: 17 | for room in live_info['room']: 18 | if room['site'] == site: 19 | live_url = room['url'] 20 | if len(live_url) == 0: 21 | raise Exception(path+'\n文件中找不到直播url地址') 22 | logmsg('获得直播url地址:'+live_url) 23 | return live_url 24 | 25 | 26 | def get_method(site='YouTube'): 27 | '''获取转播的网站,返回对应网站中获取m3u8与推流的方法 28 | ''' 29 | if site == 'YouTube': 30 | get_m3u8 = m_youtube.get_m3u8 31 | push_stream = m_youtube.push_stream 32 | return get_m3u8, push_stream 33 | 34 | 35 | def rebroadcast(live): 36 | """一次转播任务的主函数 37 | 38 | 一次转播任务分两个阶段 39 | 初始化: 40 | 登陆bilibili 41 | 从liveInfo.json中查得直播间地址 42 | 并获得直播网站的对应get_m3u8与push_stream函数 43 | 44 | 此时出现任何异常都会推出rebroadcast函数 45 | 46 | 每分钟一次,共20次循环: 47 | 获取m3u8地址 48 | 开启bilibili直播间 49 | 发送开播动态(仅一次) 50 | 开始推流 51 | 52 | 为了liver推迟开播、直播中途断开等容错 53 | 此时出现任何异常都会立即继续下一次循环 54 | 其中开播动态只会在第一次成功开始推流前发送一次 55 | 56 | 57 | Args: 58 | args: dict, 结构如下 59 | { 60 | 'time': datetime.datetime, 61 | 'liver': string, 62 | 'site': string, default='YouTube', 63 | 'title': string, default='liver+'转播', 64 | } 65 | """ 66 | args = live.args() 67 | try: 68 | logmsg('开始推流项目:\n{liver}:{site}'.format(liver=args['liver'], site=args['site'])) 69 | 70 | # Read Config 71 | config = CONFIGs() 72 | COOKIES_TXT_PATH = config.COOKIES_TXT_PATH 73 | LIVE_INFO_PATH = config.LIVE_INFO_PATH 74 | BILIBILI_ROOM_TITLE = config.BILIBILI_ROOM_TITLE 75 | FFMPEG_COMMAND = config.FFMPEG_COMMAND 76 | BILIBILI_ROOM_AREA_ID = config.BILIBILI_ROOM_AREA_ID 77 | LIVE_QUALITY = config.LIVE_QUALITY 78 | IS_SEND_PRELIVE_DYNAMIC = config.IS_SEND_PRELIVE_DYNAMIC 79 | PRELIVE_DYNAMIC_FORM = config.PRELIVE_DYNAMIC_FORM 80 | 81 | b = login_bilibili(COOKIES_TXT_PATH) 82 | live_url = get_live_url(LIVE_INFO_PATH, args['liver'], args['site']) 83 | get_m3u8, push_stream = get_method(args['site']) 84 | 85 | retry_count = 0 86 | has_posted_dynamic = False 87 | while retry_count <= 20: 88 | try: 89 | url_m3u8 = get_m3u8(live_url, LIVE_QUALITY) 90 | 91 | room_id = b.getMyRoomId() 92 | 93 | # 防止前一次直播未结束,先暂存旧直播标题,推流错误时将标题重新改回 94 | old_title = b.getRoomTitle(room_id) 95 | 96 | b.updateRoomTitle(room_id, BILIBILI_ROOM_TITLE.format( 97 | time=args['time'], 98 | liver=args['liver'], 99 | site=args['site'], 100 | title=args['title'] 101 | )) 102 | url_rtmp = b.startLive(room_id, BILIBILI_ROOM_AREA_ID) 103 | logmsg("开播成功,获得推流地址:{}".format(url_rtmp)) 104 | sleep(5) 105 | 106 | # 每次直播只发送一次动态 107 | if not has_posted_dynamic and IS_SEND_PRELIVE_DYNAMIC: 108 | dynamic_id = b.send_dynamic( 109 | PRELIVE_DYNAMIC_FORM.format( 110 | liver=args['liver'], 111 | time=args['time'].strftime(r'%m.%d %H:%M'), 112 | site=args['site'], 113 | title=args['title'], 114 | url='https://live.bilibili.com/'+str(room_id) 115 | ) 116 | ) 117 | has_posted_dynamic = True 118 | logmsg('项目{}发送动态'.format(args['liver'])) 119 | 120 | out, err, errcode = push_stream(url_rtmp, live_url, url_m3u8, FFMPEG_COMMAND) 121 | 122 | # 前一次直播未结束 123 | if errcode == 1: 124 | sleep(10) 125 | b.updateRoomTitle(room_id, old_title) 126 | sleep(60) 127 | raise Exception('直播间被占用') 128 | 129 | except Exception as e: 130 | msg = tracemsg(e) if len(str(e).strip()) == 0 else str(e) 131 | errmsg('normal', '项目:{time} {liver}\n尝试推流失败,retry_count={retry_count}\n'.format( 132 | time=args['time'], liver=args['liver'], retry_count=retry_count 133 | ) + msg) 134 | 135 | retry_count += 1 136 | sleep(60) 137 | 138 | # 关闭项目前删除已发送的动态 139 | # 若未发送动态则一定未转播成功 140 | 141 | # 但已发送动态不一定转播成功,可能是由于前一次转播未结束导致 142 | # 此BUG以后再调整 143 | if has_posted_dynamic: 144 | b.delete_dynamic(dynamic_id) 145 | logmsg('项目{}删除动态'.format(args['liver'])) 146 | """ else: 147 | b.send_dynamic( 148 | '转播失败: {liver}, {site}\n时间: {time}\n{title}'.format( 149 | liver=args['liver'], 150 | time=args['time'], 151 | title=args['title'], 152 | site=args['site'] 153 | ) 154 | ) 155 | logmsg('项目{}发送转播失败动态'.format(args['liver'])) """ 156 | 157 | except Exception as e: 158 | txt = '' 159 | if len(str(e).strip()) == 0: 160 | txt = '\n'+tracemsg(e) 161 | errmsg('schedule', str(e)+txt) 162 | 163 | logmsg('关闭推流项目:\n{liver}:{site}'.format(liver=args['liver'], site=args['site'])) -------------------------------------------------------------------------------- /src/utitls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | '''封装了程序中使用的小函数 3 | ''' 4 | from datetime import datetime 5 | from functools import wraps 6 | import json 7 | import os 8 | import traceback 9 | import subprocess 10 | 11 | 12 | def errmsg(errtype='normal', msg=""): 13 | '''按照errtype的格式,将msg记录到err.log、log.txt及打印到屏幕 14 | ''' 15 | textform={ 16 | 'normal': '[错误]{time}\n{message}\n', 17 | 'schedule': '[错误]{time}: schedule.txt不符合格式\n{message}\n', 18 | 'login': '[错误]{time}: bilibili登陆失败\n{message}\n', 19 | 'json': '[错误]{time}: json读写错误\n{message}\n', 20 | 'm3u8': '[错误]{time}: m3u8获取失败\n{message}\n', 21 | 'cmd': '[错误]{time}: 调用进程失败\n{message}\n' 22 | } 23 | msg = '\n '.join(msg.split('\n')) 24 | msg = ' ' + msg 25 | text = textform[errtype].format(time=datetime.now(), message=msg) 26 | with open('err.log', 'a', encoding='utf-8') as err: 27 | err.write(text) 28 | with open('log.txt', 'a', encoding='utf-8') as log: 29 | log.write(text) 30 | print(text) 31 | 32 | 33 | def logmsg(msg=""): 34 | '''将msg记录到log.txt及打印到屏幕 35 | ''' 36 | textform = '[记录]{time}\n{message}\n' 37 | msg = '\n '.join(msg.split('\n')) 38 | msg = ' ' + msg 39 | text = textform.format(time=datetime.now(), message=msg) 40 | with open('log.txt', 'a', encoding='utf-8') as log: 41 | log.write(text) 42 | print(text) 43 | 44 | 45 | def tracemsg(e): 46 | '''按照一定格式将Exception e的traceback输出为str 47 | ''' 48 | txt = '' 49 | for line in traceback.format_tb(e.__traceback__): 50 | txt += line 51 | return txt 52 | 53 | 54 | def dumpJson(path, data): 55 | '''按照格式将data封装为json格式并写入path中 56 | ''' 57 | with open(path, 'w', encoding='utf-8') as f: 58 | json.dump(data, f, ensure_ascii=False, indent=4) 59 | logmsg('写入json文件\n'+path) 60 | 61 | 62 | def loadJson(path): 63 | '''读取path的json文件输出为对应的数据 64 | ''' 65 | if os.path.exists(path): 66 | with open(path, encoding='utf-8') as f: 67 | return json.load(f) 68 | else: 69 | errmsg('json', path+'\n文件读入错误') 70 | return -1 71 | 72 | 73 | def RunCMD(cmd): 74 | '''调用subprocess,运行shell命令 75 | 76 | Args: 77 | cmd: str, shell命令 78 | 79 | Returns: 80 | out: 81 | err: 82 | errcode: 83 | ''' 84 | p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 85 | pid = p.pid 86 | logmsg('CMD进程开始: PID = {PID}\nCMD:"{CMD}"'.format(PID=pid, CMD=cmd)) 87 | out, err = p.communicate() 88 | errcode = p.returncode 89 | logmsg('CMD进程结束: PID = {PID}\nCMD:"{CMD}"\nERR:{ERR}\nCODE:{CODE}'.format( 90 | PID=pid, CMD=cmd, 91 | ERR=err, CODE=errcode 92 | )) 93 | return out, err, errcode 94 | 95 | 96 | def sigleton(cls): 97 | '''单例修饰器 98 | ''' 99 | __instances = {} 100 | @wraps(cls) 101 | def getinstance(*args, **kw): 102 | if cls not in __instances: 103 | __instances[cls] = cls(*args, **kw) 104 | return __instances[cls] 105 | return getinstance -------------------------------------------------------------------------------- /src/webSite/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | 3 | web = Flask(__name__) 4 | 5 | from src.webSite.views import autoLive 6 | web.register_blueprint(autoLive.mod) 7 | -------------------------------------------------------------------------------- /src/webSite/models/autoLive.py: -------------------------------------------------------------------------------- 1 | from src.utitls import loadJson 2 | from src.Configs import CONFIGs 3 | from src.liveScheduler import LiveScheduler 4 | from src.login_bilibili import login_bilibili 5 | from datetime import datetime 6 | from flask import url_for 7 | 8 | __default_table_title = ['时间', 'Vtuber', '直播网站', '自定义标题'] 9 | 10 | def header_menus(): 11 | '''返回layer模板中所用的header_menu 12 | ''' 13 | header_menus = [ 14 | { 15 | 'name': 'にじさんじ常用网站', 16 | 'Is_dropdown': True, 17 | 'Is_new_tab': True, 18 | 'contains':[ 19 | { 20 | 'name': 'YouTubeのコメントを見るやつ', 21 | 'url': r'https://2434.fun/' 22 | }, 23 | { 24 | 'name': 'にじさんじwiki', 25 | 'url': r'https://wikiwiki.jp/nijisanji/' 26 | }, 27 | { 28 | 'name': 'SEEDs24H', 29 | 'url': r'https://2434dola.wixsite.com/seeds24h-official' 30 | }, 31 | { 32 | 'name': '今週のかえみと', 33 | 'url': r'https://mato-liver.com/archives/category/kemt' 34 | }, 35 | ] 36 | }, 37 | { 38 | 'name': '时间表', 39 | 'Is_dropdown': False, 40 | 'Is_new_tab': False, 41 | 'url': url_for('autoLive.schedule') 42 | }, 43 | { 44 | 'name': '设置', 45 | 'Is_dropdown': False, 46 | 'Is_new_tab': False, 47 | 'url': url_for('autoLive.configs') 48 | } 49 | ] 50 | return header_menus 51 | 52 | def schedule_sections(): 53 | '''返回schedule页所需的数据 54 | ''' 55 | # 获得site列表 现在可用的只有YouTube 56 | sites = ['YouTube'] 57 | 58 | # 获得liver列表 59 | lives_info = loadJson(CONFIGs().LIVE_INFO_PATH) 60 | livers = [] 61 | for live_info in lives_info: 62 | livers.append(live_info['liver']) 63 | 64 | # 获得查看时间表预定中所需的列表 65 | lives = LiveScheduler().get_lives() 66 | rows = [] 67 | # 获取时将时间表按时间排序 68 | for live_id, live in sorted(lives.items(), key=lambda kv: kv[1].time): 69 | rows.append( 70 | { 71 | 'values':[ 72 | live.time.strftime(r'%m.%d %H:%M'), 73 | live.liver, 74 | live.site, 75 | live.title 76 | ], 77 | 'id': live_id 78 | } 79 | ) 80 | 81 | # 获得查看运行中项目所需的job列表 82 | jobs = [] 83 | for live_id, job in LiveScheduler().get_livings().items(): 84 | running_rows=[ 85 | {'title': 'YouTuber', 'value': job['live'].liver}, 86 | {'title': '直播网站', 'value': job['live'].site}, 87 | {'title': '直播间自定义标题', 'value': job['live'].title}, 88 | {'title': '预计开始时间', 'value': job['live'].time}, 89 | {'title': '实际开始时间', 'value': job['startT']}, 90 | {'title': '持续时间', 'value': datetime.now() - job['startT']} 91 | ] 92 | jobs.append( 93 | { 94 | 'title': live_id, 95 | 'rows': running_rows 96 | } 97 | ) 98 | 99 | # 添加直播项中需要的值 100 | add_job_value = { 101 | 'title': '添加新项目', 102 | 'descr': '添加时间表项目。\n'\ 103 | '\n'\ 104 | '时间一栏可填入以下内容:\n'\ 105 | '1. "now": 程序会自动将开播时间定为点击提交的30秒后。\n'\ 106 | '2. 填入HHMM格式的四位数字,代表未来24小时内对应日本时区的时间。\n'\ 107 | '目前只能接受未来24小时内开始的直播。\n'\ 108 | '\n'\ 109 | '自定义标题可不填,不填时默认值为"YouTuber名+转播"。', 110 | 'table_titles': __default_table_title, 111 | 'row': [ 112 | {'input_type': 'text', 'name': 'time'}, 113 | {'input_type': 'select', 'name': 'liver', 'contains': livers}, 114 | {'input_type': 'select', 'name': 'site', 'contains': sites}, 115 | {'input_type': 'text', 'name': 'title'} 116 | ] 117 | } 118 | 119 | # 查看时间表中需要的值 120 | schedule_jobs_value = { 121 | 'title': '已注册项目', 122 | 'descr': '时间表中预定要运行的项目。\n'\ 123 | '时间一栏为日本时区时间。\n'\ 124 | '每天会在本地时间下午15时在B站发送一次每日时间表动态,内容即为当时此时间表内的内容。\n'\ 125 | '表内的项目不一定都能转播成功,直播更改日期或是延迟20分钟以上都不能转播成功。', 126 | 'table_titles': __default_table_title, 127 | 'rows': rows 128 | } 129 | 130 | # 查看运行中项目所需要的值 131 | running_jobs_value = { 132 | 'title': '运行中的项目', 133 | 'descr': '正在运行中的项目。\n'\ 134 | '其中的项目不一定都正在转播,有可能是直播开始前正在尝试开始转播,或是直播结束后正在尝试掉线重连。', 135 | 'jobs': jobs 136 | } 137 | 138 | sections = { 139 | 'add_job': add_job_value, 140 | 'schedule_jobs': schedule_jobs_value, 141 | 'running_jobs': running_jobs_value 142 | } 143 | return sections 144 | 145 | 146 | def configs_sections(): 147 | '''返回设置页面所需数据 148 | ''' 149 | config = CONFIGs() 150 | 151 | # BILIBILI_ROOM_AREA_ID所需的分区列表 152 | b = login_bilibili(config.COOKIES_TXT_PATH) 153 | area_list_data = b.getLiveAreaList() 154 | area_list = [] 155 | for zone in area_list_data: 156 | for area in zone['list']: 157 | if area['lock_status'] == '0': 158 | area_list.append( 159 | {'value': int(area['id']), 'display': area['name']} 160 | ) 161 | 162 | # bilibili 163 | item_BILIBILI_ROOM_TITLE = { 164 | 'title': '直播间标题格式', 165 | 'input_type': 'text', 166 | 'name': 'BILIBILI_ROOM_TITLE', 167 | 'value': config.BILIBILI_ROOM_TITLE 168 | } 169 | item_DEFAULT_TITLE_PARAM = { 170 | 'title': '默认title参数格式', 171 | 'input_type': 'text', 172 | 'name': 'DEFAULT_TITLE_PARAM', 173 | 'value': config.DEFAULT_TITLE_PARAM 174 | } 175 | item_BILIBILI_ROOM_AREA_ID = { 176 | 'title': '直播间分区', 177 | 'input_type': 'select', 178 | 'name': 'BILIBILI_ROOM_AREA_ID', 179 | 'value': config.BILIBILI_ROOM_AREA_ID, 180 | 'options': area_list 181 | } 182 | item_IS_SEND_DAILY_DYNAMIC = { 183 | 'title': '发送每日转播列表动态', 184 | 'input_type': 'checkbox', 185 | 'name': 'IS_SEND_DAILY_DYNAMIC', 186 | 'value': config.IS_SEND_DAILY_DYNAMIC 187 | } 188 | item_DAILY_DYNAMIC_FORM = { 189 | 'title': '每日转播列表动态格式', 190 | 'input_type': 'text', 191 | 'name': 'DAILY_DYNAMIC_FORM', 192 | 'value': config.DAILY_DYNAMIC_FORM 193 | } 194 | item_IS_SEND_PRELIVE_DYNAMIC = { 195 | 'title': '转播前发送动态', 196 | 'input_type': 'checkbox', 197 | 'name': 'IS_SEND_PRELIVE_DYNAMIC', 198 | 'value': config.IS_SEND_PRELIVE_DYNAMIC 199 | } 200 | item_PRELIVE_DYNAMIC_FORM = { 201 | 'title': '转播前动态格式', 202 | 'input_type': 'text', 203 | 'name': 'PRELIVE_DYNAMIC_FORM', 204 | 'value': config.PRELIVE_DYNAMIC_FORM 205 | } 206 | section_bilibili = { 207 | 'title': 'BILIBILI', 208 | 'descr': 'B站直播间与动态相关设置。\n'\ 209 | '“直播间标题格式”中可使用参数: {time}, {liver}, {site}, {title}\n'\ 210 | '分别意义为:直播开始时间、主播、直播网站、添加直播项时填入的自定义标题。\n'\ 211 | '\n'\ 212 | '“默认title参数格式”中可使用参数:{time}, {liver}, {site}\n'\ 213 | '当添加直播项中“自定义标题”一栏为空时,会填入此处的默认title参数。\n'\ 214 | '\n'\ 215 | '如果勾选“发送每日转播列表动态”,会在每天本地时间15时发送一个B站动态。\n'\ 216 | '内容为当时时间表内所有的已注册项目,并且每个项目套用“每日转播列表动态格式”,可使用\\n转义。\n'\ 217 | '“每日转播列表动态格式”中可使用参数:{time}, {liver}, {site}, {title}\n'\ 218 | '如果当时已注册项目列表为空,则就算勾选了“发送每日转播列表动态”也不会发送动态。\n'\ 219 | '\n'\ 220 | '如果勾选“转播前发送动态”,则在直播前会发送一个B站动态。\n'\ 221 | '内容会套用“转播前动态格式”,并且会在转播任务结束后自动删除。\n'\ 222 | '“转播前动态格式”中可使用参数:{time}, {liver}, {site}, {title}, {url}\n'\ 223 | '其中url参数意义为直播间链接,其他四项与上面相同。', 224 | 'items': [ 225 | item_BILIBILI_ROOM_TITLE, 226 | item_DEFAULT_TITLE_PARAM, 227 | item_BILIBILI_ROOM_AREA_ID, 228 | item_IS_SEND_DAILY_DYNAMIC, 229 | item_DAILY_DYNAMIC_FORM, 230 | item_IS_SEND_PRELIVE_DYNAMIC, 231 | item_PRELIVE_DYNAMIC_FORM, 232 | ] 233 | } 234 | 235 | # liveParam 236 | item_LIVE_QUALITY = { 237 | 'title': '最高清晰度', 238 | 'input_type': 'select', 239 | 'name': 'LIVE_QUALITY', 240 | 'value': config.LIVE_QUALITY, 241 | 'options': [ 242 | {'value': 0, 'display': 0}, 243 | {'value': 240, 'display': 240}, 244 | {'value': 360, 'display': 360}, 245 | {'value': 480, 'display': 480}, 246 | {'value': 720, 'display': 720}, 247 | {'value': 1080, 'display': 1080}, 248 | ] 249 | } 250 | section_liveParam = { 251 | 'title': 'liveParam', 252 | 'descr': '清晰度等直播相关设定\n'\ 253 | '转播清晰度会尽量取最大,但不会超过“最高清晰度”,请根据自己服务器的网络情况来选取。\n'\ 254 | '如果“最高清晰度”设为0,即为不设清晰度上限,转播清晰度永远取最大值。', 255 | 'items': [ 256 | item_LIVE_QUALITY, 257 | ] 258 | } 259 | 260 | sections = { 261 | 'bilibili': section_bilibili, 262 | 'liveParam': section_liveParam, 263 | } 264 | return sections -------------------------------------------------------------------------------- /src/webSite/static/css/cascade.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * LuCI Bootstrap Theme 3 | * Copyright 2012 Nut & Bolt 4 | * By David Menting 5 | * Based on Bootstrap v1.4.0 6 | * 7 | * Copyright 2011 Twitter, Inc 8 | * Licensed under the Apache License v2.0 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 12 | */ 13 | /* Reset.less 14 | * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc). 15 | * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */ 16 | html { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | 21 | body { 22 | margin: 0; 23 | padding: 5px; 24 | } 25 | 26 | h1, h2, h3, h4, h5, h6, p, pre, a, abbr, acronym, code, del, em, img, q, s, 27 | small, strike, strong, sub, sup, tt, var, dd, dl, dt, li, ol, ul, fieldset, 28 | form, label, legend, button, table, caption, tbody, tfoot, thead, tr, th, td, 29 | .table, .tbody, .tfoot, .thead, .tr, .th, .td { 30 | margin: 0; 31 | padding: 0; 32 | border: 0; 33 | font-weight: normal; 34 | font-style: normal; 35 | font-size: 100%; 36 | line-height: 1; 37 | font-family: inherit; 38 | } 39 | 40 | abbr[title], acronym[title] { 41 | border-bottom: 1px dotted; 42 | font-weight: inherit; 43 | cursor: help; 44 | } 45 | 46 | table { 47 | border-collapse: collapse; 48 | border-spacing: 0; 49 | } 50 | 51 | ol, ul { 52 | list-style: none; 53 | } 54 | 55 | q:before, 56 | q:after, 57 | blockquote:before, 58 | blockquote:after { 59 | content: ""; 60 | } 61 | 62 | html { 63 | overflow-y: scroll; 64 | font-size: 100%; 65 | -webkit-text-size-adjust: 100%; 66 | -ms-text-size-adjust: 100%; 67 | } 68 | 69 | a:focus { 70 | outline: thin dotted; 71 | } 72 | 73 | a:hover, a:active { 74 | outline: 0; 75 | } 76 | 77 | article, 78 | aside, 79 | details, 80 | figcaption, 81 | figure, 82 | footer, 83 | header, 84 | hgroup, 85 | nav, 86 | section { 87 | display: block; 88 | } 89 | 90 | sub, sup { 91 | font-size: 75%; 92 | line-height: 0; 93 | position: relative; 94 | vertical-align: baseline; 95 | } 96 | 97 | sup { 98 | top: -0.5em; 99 | } 100 | 101 | sub { 102 | bottom: -0.25em; 103 | } 104 | 105 | img { 106 | border: 0; 107 | -ms-interpolation-mode: bicubic; 108 | } 109 | 110 | button, 111 | input, 112 | select, 113 | option, 114 | textarea { 115 | font-size: 100%; 116 | margin: 0; 117 | box-sizing: border-box; 118 | vertical-align: baseline; 119 | *vertical-align: middle; 120 | } 121 | 122 | button, input { 123 | line-height: normal; 124 | *overflow: visible; 125 | } 126 | 127 | button::-moz-focus-inner, input::-moz-focus-inner { 128 | border: 0; 129 | padding: 0; 130 | } 131 | 132 | button, 133 | input[type="button"], 134 | input[type="reset"], 135 | input[type="submit"] { 136 | cursor: pointer; 137 | -webkit-appearance: button; 138 | } 139 | 140 | button[disabled], 141 | input[type="button"][disabled], 142 | input[type="reset"][disabled], 143 | input[type="submit"][disabled] { 144 | opacity: 0.7; 145 | } 146 | 147 | input[type="search"] { 148 | -webkit-appearance: textfield; 149 | box-sizing: content-box; 150 | } 151 | 152 | input[type="search"]::-webkit-search-decoration { 153 | -webkit-appearance: none; 154 | } 155 | 156 | textarea { 157 | overflow: auto; 158 | vertical-align: top; 159 | } 160 | 161 | /* 162 | * Scaffolding 163 | * Basic and global styles for generating a grid system, structural layout, and page templates 164 | * ------------------------------------------------------------------------------------------- */ 165 | body { 166 | background-color: #fff; 167 | margin: 0; 168 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 169 | font-size: 13px; 170 | font-weight: normal; 171 | line-height: 18px; 172 | color: #404040; 173 | padding-top: 58px; 174 | } 175 | 176 | .container { 177 | width: 100%; 178 | max-width: 940px; 179 | margin-left: auto; 180 | margin-right: auto; 181 | zoom: 1; 182 | } 183 | 184 | .container:before, .container:after { 185 | display: table; 186 | content: ""; 187 | zoom: 1; 188 | } 189 | 190 | .container:after { 191 | clear: both; 192 | } 193 | 194 | a { 195 | color: #0069d6; 196 | text-decoration: none; 197 | line-height: inherit; 198 | font-weight: inherit; 199 | } 200 | 201 | a:hover { 202 | color: #00438a; 203 | text-decoration: underline; 204 | } 205 | 206 | .pull-right { 207 | float: right; 208 | } 209 | 210 | .pull-left { 211 | float: left; 212 | } 213 | 214 | /* Typography.less 215 | * Headings, body text, lists, code, and more for a versatile and durable typography system 216 | * ---------------------------------------------------------------------------------------- */ 217 | p, 218 | .cbi-map-descr, 219 | .cbi-section-descr, 220 | .table .tr.cbi-section-table-descr .th { 221 | font-size: 13px; 222 | font-weight: normal; 223 | line-height: 18px; 224 | margin-bottom: 9px; 225 | } 226 | 227 | p small { 228 | font-size: 11px; 229 | color: #bfbfbf; 230 | } 231 | 232 | h1, 233 | h2, 234 | h3, legend, 235 | h4, 236 | h5, 237 | h6 { 238 | font-weight: bold; 239 | color: #404040; 240 | } 241 | 242 | h1 small, 243 | h2 small, 244 | h3 small, 245 | h4 small, 246 | h5 small, 247 | h6 small { 248 | color: #bfbfbf; 249 | } 250 | 251 | h1 { 252 | margin-bottom: 18px; 253 | font-size: 30px; 254 | line-height: 36px; 255 | } 256 | 257 | h1 small { 258 | font-size: 18px; 259 | } 260 | 261 | h2 { 262 | font-size: 24px; 263 | line-height: 36px; 264 | } 265 | 266 | h2 small { 267 | font-size: 14px; 268 | } 269 | 270 | h3, legend, 271 | h4, 272 | h5, 273 | h6 { 274 | line-height: 36px; 275 | } 276 | 277 | h3, legend { 278 | font-size: 18px; 279 | } 280 | 281 | h3 small { 282 | font-size: 14px; 283 | } 284 | 285 | h4 { 286 | font-size: 16px; 287 | } 288 | 289 | h4 small { 290 | font-size: 12px; 291 | } 292 | 293 | h5 { 294 | font-size: 14px; 295 | } 296 | 297 | h6 { 298 | font-size: 13px; 299 | color: #bfbfbf; 300 | text-transform: uppercase; 301 | } 302 | 303 | ul, ol { 304 | margin: 0 0 18px 25px; 305 | } 306 | 307 | ul ul, 308 | ul ol, 309 | ol ol, 310 | ol ul { 311 | margin-bottom: 0; 312 | } 313 | 314 | ul { 315 | list-style: disc; 316 | } 317 | 318 | ol { 319 | list-style: decimal; 320 | } 321 | 322 | li { 323 | line-height: 18px; 324 | color: #808080; 325 | } 326 | 327 | ul.unstyled { 328 | list-style: none; 329 | margin-left: 0; 330 | } 331 | 332 | dl { 333 | margin-bottom: 18px; 334 | } 335 | 336 | dl dt, dl dd { 337 | line-height: 18px; 338 | } 339 | 340 | dl dt { 341 | font-weight: bold; 342 | } 343 | 344 | dl dd { 345 | margin-left: 9px; 346 | } 347 | 348 | hr { 349 | margin: 20px 0 19px; 350 | border: 0; 351 | border-bottom: 1px solid #eee; 352 | } 353 | 354 | strong { 355 | font-style: inherit; 356 | font-weight: bold; 357 | } 358 | 359 | em { 360 | font-style: italic; 361 | font-weight: inherit; 362 | line-height: inherit; 363 | } 364 | 365 | small { font-size: 0.9em } 366 | 367 | address { 368 | display: block; 369 | line-height: 18px; 370 | margin-bottom: 18px; 371 | } 372 | 373 | code, pre { 374 | padding: 0 3px 2px; 375 | font-family: Monaco, Andale Mono, Courier New, monospace; 376 | font-size: 12px; 377 | border-radius: 3px; 378 | } 379 | 380 | code { 381 | background-color: #fee9cc; 382 | color: rgba(0, 0, 0, 0.75); 383 | padding: 1px 3px; 384 | } 385 | 386 | pre { 387 | background-color: #f5f5f5; 388 | display: block; 389 | padding: 8.5px; 390 | margin: 0 0 18px; 391 | line-height: 18px; 392 | font-size: 12px; 393 | border: 1px solid #ccc; 394 | border: 1px solid rgba(0, 0, 0, 0.15); 395 | border-radius: 3px; 396 | white-space: pre; 397 | white-space: pre-wrap; 398 | word-wrap: break-word; 399 | } 400 | 401 | /* Forms.less 402 | * Base styles for various input types, form layouts, and states 403 | * ------------------------------------------------------------- */ 404 | form { 405 | margin-bottom: 18px; 406 | } 407 | 408 | fieldset { 409 | margin-bottom: 9px; 410 | padding-top: 9px; 411 | } 412 | 413 | fieldset legend { 414 | display: block; 415 | font-size: 19.5px; 416 | line-height: 1; 417 | color: #404040; 418 | padding-top: 20px; 419 | *padding: 0 0 5px 0px; 420 | /* IE6-7 */ 421 | 422 | *line-height: 1.5; 423 | /* IE6-7 */ 424 | 425 | } 426 | form .cbi-tab-descr { 427 | line-height: 18px; 428 | margin-bottom: 18px; 429 | } 430 | 431 | form .clearfix, 432 | form .cbi-value { 433 | margin-bottom: 18px; 434 | zoom: 1; 435 | } 436 | 437 | form .clearfix:before, form .clearfix:after, 438 | form .cbi-value:before, form .cbi-value:after { 439 | display: table; 440 | content: ""; 441 | zoom: 1; 442 | } 443 | 444 | form .clearfix:after, 445 | form .cbi-value:after { 446 | clear: both; 447 | } 448 | 449 | label, 450 | input, 451 | select, 452 | textarea { 453 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 454 | font-size: 13px; 455 | font-weight: normal; 456 | line-height: normal; 457 | } 458 | 459 | form .input, 460 | form .cbi-value-field { 461 | margin-left: 200px; 462 | } 463 | 464 | form .cbi-value label.cbi-value-title { 465 | padding-top: 6px; 466 | font-size: 13px; 467 | line-height: 18px; 468 | float: left; 469 | width: 180px; 470 | text-align: right; 471 | color: #404040; 472 | } 473 | 474 | input[type=checkbox], input[type=radio] { 475 | cursor: pointer; 476 | } 477 | 478 | input, 479 | textarea, 480 | select, 481 | .cbi-dropdown, 482 | .uneditable-input { 483 | display: inline-block; 484 | width: 210px; 485 | height: 30px; 486 | padding: 4px; 487 | font-size: 13px; 488 | line-height: 18px; 489 | color: #808080; 490 | border: 1px solid #ccc; 491 | border-radius: 3px; 492 | box-sizing: border-box; 493 | } 494 | 495 | .cbi-dropdown { 496 | min-width: 210px; 497 | max-width: 400px; 498 | width: auto; 499 | } 500 | 501 | select { 502 | padding: initial; 503 | background: #fff; 504 | box-shadow: inset 0 -1px 3px rgba(0, 0, 0, 0.1); 505 | } 506 | 507 | input[type=checkbox], input[type=radio] { 508 | width: auto; 509 | height: auto; 510 | padding: 0; 511 | margin: 3px 0; 512 | *margin-top: 0; 513 | /* IE6-7 */ 514 | 515 | line-height: normal; 516 | border: none; 517 | } 518 | 519 | input[type=file] { 520 | background-color: #fff; 521 | padding: initial; 522 | border: initial; 523 | line-height: initial; 524 | box-shadow: none; 525 | width: auto !important; 526 | } 527 | 528 | input[type=button], input[type=reset], input[type=submit] { 529 | width: auto; 530 | height: auto; 531 | } 532 | 533 | select, input[type=file] { 534 | *height: auto; 535 | *margin-top: 4px; 536 | /* For IE7, add top margin to align select with labels */ 537 | } 538 | 539 | select[multiple] { 540 | height: inherit; 541 | background-color: #fff; 542 | } 543 | 544 | textarea { 545 | height: auto; 546 | } 547 | 548 | .td > input[type=text], 549 | .td > input[type=password], 550 | .td > select, 551 | .td > .cbi-dropdown { 552 | width: 100%; 553 | } 554 | 555 | .uneditable-input { 556 | background-color: #fff; 557 | display: block; 558 | border-color: #eee; 559 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025); 560 | cursor: not-allowed; 561 | } 562 | 563 | ::-moz-placeholder { 564 | color: #bfbfbf; 565 | } 566 | 567 | ::-webkit-input-placeholder { 568 | color: #bfbfbf; 569 | } 570 | 571 | .btn, .cbi-button, input, textarea { 572 | transition: border linear 0.2s, box-shadow linear 0.2s; 573 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); 574 | } 575 | 576 | .btn:hover, .cbi-button:hover, 577 | input:focus, textarea:focus { 578 | outline: 0; 579 | border-color: rgba(82, 168, 236, 0.8) !important; 580 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6); 581 | text-decoration: none; 582 | } 583 | 584 | input[type=file]:focus, input[type=checkbox]:focus, select:focus { 585 | box-shadow: none; 586 | outline: 1px dotted #666; 587 | } 588 | 589 | input[disabled], 590 | select[disabled], 591 | textarea[disabled], 592 | input[readonly], 593 | select[readonly], 594 | textarea[readonly] { 595 | background-color: #f5f5f5; 596 | border-color: #ddd; 597 | pointer-events: none; 598 | cursor: default; 599 | } 600 | 601 | select[readonly], 602 | textarea[readonly] { 603 | pointer-events: auto; 604 | cursor: auto; 605 | } 606 | 607 | .cbi-optionals, 608 | .cbi-section-create { 609 | padding: 0 0 10px 10px; 610 | } 611 | 612 | .cbi-section-create { 613 | margin: -3px; 614 | display: inline-flex; 615 | align-items: center; 616 | } 617 | 618 | .cbi-section-create > * { 619 | margin: 3px; 620 | flex: 1 1 auto; 621 | } 622 | 623 | .cbi-section-create > * > input { 624 | width: 100%; 625 | } 626 | 627 | .actions, 628 | .cbi-page-actions { 629 | background: #f5f5f5; 630 | margin-bottom: 18px; 631 | padding: 17px 20px 18px 17px; 632 | border-top: 1px solid #ddd; 633 | border-radius: 0 0 3px 3px; 634 | text-align: right; 635 | } 636 | 637 | .actions .secondary-action, 638 | .cbi-page-actions .secondary-action{ 639 | float: right; 640 | } 641 | 642 | .actions .secondary-action a, 643 | .cbi-page-actions .secondary-action a { 644 | line-height: 30px; 645 | } 646 | 647 | .actions .secondary-action a:hover, 648 | .cbi-page-actions .secondary-action a:hover { 649 | text-decoration: underline; 650 | } 651 | 652 | .cbi-page-actions > form { 653 | display: inline; 654 | margin: 0; 655 | } 656 | 657 | .help-inline, .help-block { 658 | font-size: 13px; 659 | line-height: 18px; 660 | color: #bfbfbf; 661 | } 662 | 663 | .help-inline { 664 | padding-left: 5px; 665 | *position: relative; 666 | /* IE6-7 */ 667 | 668 | *top: -5px; 669 | /* IE6-7 */ 670 | 671 | } 672 | 673 | .help-block { 674 | display: block; 675 | max-width: 600px; 676 | } 677 | 678 | /* 679 | * Tables.less 680 | * Tables for, you guessed it, tabular data 681 | * ---------------------------------------- */ 682 | .tr { display: table-row; } 683 | .table[width="33%"], .th[width="33%"], .td[width="33%"] { width: 33%; } 684 | .table[width="100%"], .th[width="100%"], .td[width="100%"] { width: 100%; } 685 | 686 | .table { 687 | display: table; 688 | width: 100%; 689 | margin-bottom: 18px; 690 | padding: 0; 691 | font-size: 13px; 692 | border-collapse: collapse; 693 | position: relative; 694 | } 695 | 696 | .table .th, .table .td { 697 | display: table-cell; 698 | vertical-align: middle; /* Fixme */ 699 | padding: 10px 10px 9px; 700 | line-height: 18px; 701 | text-align: left; 702 | } 703 | 704 | .table .tr:first-child .th { 705 | padding-top: 9px; 706 | font-weight: bold; 707 | vertical-align: top; 708 | } 709 | 710 | .table .td, .table .th { 711 | border-top: 1px solid #ddd; 712 | } 713 | 714 | .tr.placeholder { 715 | height: calc(3em + 20px); 716 | } 717 | 718 | .tr.placeholder > .td { 719 | position: absolute; 720 | left: 0; 721 | right: 0; 722 | bottom: 0; 723 | text-align: center; 724 | line-height: 3em; 725 | } 726 | 727 | /* Patterns.less 728 | * Repeatable UI elements outside the base styles provided from the scaffolding 729 | * ---------------------------------------------------------------------------- */ 730 | header { 731 | height: 40px; 732 | position: fixed; 733 | top: 0; 734 | left: 0; 735 | right: 0; 736 | z-index: 10000; 737 | overflow: visible; 738 | color: #BFBFBF; 739 | } 740 | 741 | header a { 742 | color: #bfbfbf; 743 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 744 | } 745 | 746 | header h3 a:hover, header .brand:hover, header ul .active > a { 747 | background-color: #333; 748 | background-color: rgba(255, 255, 255, 0.05); 749 | color: #fff; 750 | text-decoration: none; 751 | } 752 | 753 | header h3 { 754 | position: relative; 755 | } 756 | 757 | header h3 a, header .brand { 758 | float: left; 759 | display: block; 760 | padding: 8px 20px 12px; 761 | margin-left: -20px; 762 | color: #fff; 763 | font-size: 20px; 764 | font-weight: 200; 765 | line-height: 1; 766 | } 767 | 768 | header p { 769 | margin: 0; 770 | line-height: 40px; 771 | } 772 | 773 | header .fill { 774 | background-color: #222; 775 | background-repeat: repeat-x; 776 | background-image: linear-gradient(to bottom, #333333, #222222); 777 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25), inset 0 -1px 0 rgba(0, 0, 0, 0.1); 778 | padding: 0 5px; 779 | } 780 | 781 | header div > ul, .nav { 782 | display: block; 783 | float: left; 784 | margin: 0 10px 0 0; 785 | position: relative; 786 | left: 0; 787 | } 788 | 789 | header div > ul > li, .nav > li { 790 | display: block; 791 | float: left; 792 | } 793 | 794 | header div > ul a, .nav a { 795 | display: block; 796 | float: none; 797 | padding: 10px 10px 11px; 798 | line-height: 19px; 799 | text-decoration: none; 800 | } 801 | 802 | header div > ul a:hover, .nav a:hover { 803 | color: #fff; 804 | text-decoration: none; 805 | } 806 | 807 | header div > ul .active > a, .nav .active > a { 808 | background-color: #222; 809 | background-color: rgba(0, 0, 0, 0.5); 810 | } 811 | 812 | header div > ul.secondary-nav, .nav.secondary-nav { 813 | float: right; 814 | margin-left: 10px; 815 | margin-right: 0; 816 | } 817 | 818 | header div > ul.secondary-nav .menu-dropdown, 819 | .nav.secondary-nav .menu-dropdown, 820 | header div > ul.secondary-nav .dropdown-menu, 821 | .nav.secondary-nav .dropdown-menu { 822 | right: 0; 823 | border: 0; 824 | } 825 | 826 | header div > ul a.menu:hover, 827 | .nav a.menu:hover, 828 | header div > ul li.open .menu, 829 | .nav li.open .menu, 830 | header div > ul .dropdown-toggle:hover, 831 | .nav .dropdown-toggle:hover, 832 | header div > ul .dropdown.open .dropdown-toggle, 833 | .nav .dropdown.open .dropdown-toggle { 834 | background: #444; 835 | background: rgba(255, 255, 255, 0.05); 836 | } 837 | 838 | header div > ul .menu-dropdown, 839 | .nav .menu-dropdown, 840 | header div > ul .dropdown-menu, 841 | .nav .dropdown-menu { 842 | background-color: #333; 843 | } 844 | 845 | header div > ul .menu-dropdown a.menu, 846 | .nav .menu-dropdown a.menu, 847 | header div > ul .dropdown-menu a.menu, 848 | .nav .dropdown-menu a.menu, 849 | header div > ul .menu-dropdown .dropdown-toggle, 850 | .nav .menu-dropdown .dropdown-toggle, 851 | header div > ul .dropdown-menu .dropdown-toggle, 852 | .nav .dropdown-menu .dropdown-toggle { 853 | color: #fff; 854 | } 855 | 856 | header div > ul .menu-dropdown a.menu.open, 857 | .nav .menu-dropdown a.menu.open, 858 | header div > ul .dropdown-menu a.menu.open, 859 | .nav .dropdown-menu a.menu.open, 860 | header div > ul .menu-dropdown .dropdown-toggle.open, 861 | .nav .menu-dropdown .dropdown-toggle.open, 862 | header div > ul .dropdown-menu .dropdown-toggle.open, 863 | .nav .dropdown-menu .dropdown-toggle.open { 864 | background: #444; 865 | background: rgba(255, 255, 255, 0.05); 866 | } 867 | 868 | header div > ul .menu-dropdown li a, 869 | .nav .menu-dropdown li a, 870 | header div > ul .dropdown-menu li a, 871 | .nav .dropdown-menu li a { 872 | color: #999; 873 | text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5); 874 | } 875 | 876 | header div > ul .menu-dropdown li a:hover, 877 | .nav .menu-dropdown li a:hover, 878 | header div > ul .dropdown-menu li a:hover, 879 | .nav .dropdown-menu li a:hover { 880 | background-color: #191919; 881 | background-repeat: repeat-x; 882 | background-image: linear-gradient(to bottom, #292929, #191919); 883 | color: #fff; 884 | } 885 | 886 | header div > ul .menu-dropdown .active a, 887 | .nav .menu-dropdown .active a, 888 | header div > ul .dropdown-menu .active a, 889 | .nav .dropdown-menu .active a { 890 | color: #fff; 891 | } 892 | 893 | header div > ul .menu-dropdown .divider, 894 | .nav .menu-dropdown .divider, 895 | header div > ul .dropdown-menu .divider, 896 | .nav .dropdown-menu .divider { 897 | background-color: #222; 898 | border-color: #444; 899 | } 900 | 901 | header ul .menu-dropdown li a, header ul .dropdown-menu li a { 902 | padding: 4px 15px; 903 | } 904 | 905 | li.menu, .dropdown { 906 | position: relative; 907 | } 908 | 909 | a.menu:after, .dropdown-toggle:after { 910 | width: 0; 911 | height: 0; 912 | display: inline-block; 913 | content: "↓"; 914 | text-indent: -99999px; 915 | vertical-align: top; 916 | margin-top: 8px; 917 | margin-left: 4px; 918 | border-left: 4px solid transparent; 919 | border-right: 4px solid transparent; 920 | border-top: 4px solid #fff; 921 | opacity: 0.5; 922 | } 923 | 924 | .menu-dropdown, .dropdown-menu { 925 | background-color: #fff; 926 | float: left; 927 | position: absolute; 928 | top: 40px; 929 | left: -9999px; 930 | z-index: 900; 931 | min-width: 160px; 932 | max-width: 220px; 933 | _width: 160px; 934 | margin-left: 0; 935 | margin-right: 0; 936 | padding: 6px 0; 937 | zoom: 1; 938 | border-color: #999; 939 | border-color: rgba(0, 0, 0, 0.2); 940 | border-style: solid; 941 | border-width: 0 1px 1px; 942 | border-radius: 0 0 6px 6px; 943 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); 944 | background-clip: padding-box; 945 | } 946 | 947 | .menu-dropdown li, .dropdown-menu li { 948 | float: none; 949 | display: block; 950 | background-color: transparent; 951 | } 952 | 953 | .menu-dropdown .divider, .dropdown-menu .divider { 954 | height: 1px; 955 | margin: 5px 0; 956 | overflow: hidden; 957 | background-color: #eee; 958 | border-bottom: 1px solid #fff; 959 | } 960 | 961 | header .dropdown-menu a, .dropdown-menu a { 962 | display: block; 963 | padding: 4px 15px; 964 | clear: both; 965 | font-weight: normal; 966 | line-height: 18px; 967 | color: #808080; 968 | text-shadow: 0 1px 0 #fff; 969 | } 970 | 971 | header .dropdown-menu a:hover, 972 | .dropdown-menu a:hover, 973 | header .dropdown-menu a.hover, 974 | .dropdown-menu a.hover { 975 | background-color: #ddd; 976 | background-repeat: repeat-x; 977 | background-image: linear-gradient(to bottom, #eee, #ddd); 978 | color: #404040; 979 | text-decoration: none; 980 | box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.025), inset 0 -1px rgba(0, 0, 0, 0.025); 981 | } 982 | 983 | .open .menu, 984 | .dropdown.open .menu, 985 | .open .dropdown-toggle, 986 | .dropdown.open .dropdown-toggle { 987 | color: #fff; 988 | background: #ccc; 989 | background: rgba(0, 0, 0, 0.3); 990 | } 991 | 992 | .open .menu-dropdown, 993 | .dropdown.open .menu-dropdown, 994 | .open .dropdown-menu, 995 | .dropdown.open .dropdown-menu { 996 | left: 0; 997 | } 998 | 999 | .dropdown:hover ul.dropdown-menu { 1000 | left: 0; 1001 | } 1002 | 1003 | .dropdown-menu .dropdown-menu { 1004 | position: absolute; 1005 | left: 159px; 1006 | } 1007 | 1008 | .dropdown-menu li { 1009 | position: relative; 1010 | } 1011 | 1012 | .tabs, .cbi-tabmenu { 1013 | margin: 0 0 18px; 1014 | padding: 0; 1015 | list-style: none; 1016 | zoom: 1; 1017 | } 1018 | 1019 | .tabs:before, 1020 | .cbi-tabmenu:before, 1021 | .tabs:after, 1022 | .cbi-tabmenu:after { 1023 | display: table; 1024 | content: ""; 1025 | zoom: 1; 1026 | } 1027 | 1028 | .tabs:after, .cbi-tabmenu:after { 1029 | clear: both; 1030 | } 1031 | 1032 | .tabs > li, .cbi-tabmenu > li { 1033 | float: left; 1034 | } 1035 | 1036 | .tabs > li > a, .cbi-tabmenu > li > a { 1037 | display: block; 1038 | } 1039 | 1040 | .tabs, 1041 | .cbi-tabmenu { 1042 | border-color: #ddd; 1043 | border-style: solid; 1044 | border-width: 0 0 1px; 1045 | } 1046 | 1047 | .tabs > li, 1048 | .cbi-tabmenu > li { 1049 | position: relative; 1050 | margin-bottom: -1px; 1051 | } 1052 | 1053 | .cbi-tabmenu.map { 1054 | margin: 0; 1055 | } 1056 | 1057 | .cbi-tabmenu.map > li { 1058 | font-size: 16.5px; 1059 | font-weight: bold; 1060 | } 1061 | 1062 | .cbi-tabcontainer > fieldset.cbi-section[id] > legend { 1063 | display: none; 1064 | } 1065 | 1066 | .tabs > li > a, 1067 | .cbi-tabmenu > li > a { 1068 | padding: 0 15px; 1069 | margin-right: 2px; 1070 | line-height: 34px; 1071 | border: 1px solid transparent; 1072 | border-radius: 4px 4px 0 0; 1073 | } 1074 | 1075 | .tabs > li > a:hover, 1076 | .cbi-tabmenu > li > a:hover { 1077 | text-decoration: none; 1078 | background-color: #eee; 1079 | border-color: #eee #eee #ddd; 1080 | } 1081 | 1082 | .tabs .active > a, .tabs .active > a:hover, 1083 | .cbi-tabmenu .active > a, .cbi-tabmenu .active > a:hover, 1084 | .cbi-tab > a:link, .cbi-tab > a:hover { 1085 | color: #808080; 1086 | background-color: #fff; 1087 | border: 1px solid #ddd; 1088 | border-bottom-color: transparent; 1089 | cursor: default; 1090 | } 1091 | 1092 | .tabs .menu-dropdown, .tabs .dropdown-menu, 1093 | .cbi-tabmenu .menu-dropdown, .cbi-tabmenu .dropdown-menu { 1094 | top: 35px; 1095 | border-width: 1px; 1096 | border-radius: 0 6px 6px 6px; 1097 | } 1098 | 1099 | .tabs a.menu:after, .tabs .dropdown-toggle:after, 1100 | .cbi-tabmenu a.menu:after, .cbi-tabmenu .dropdown-toggle:after { 1101 | border-top-color: #999; 1102 | margin-top: 15px; 1103 | margin-left: 5px; 1104 | } 1105 | 1106 | .tabs li.open.menu .menu, .tabs .open.dropdown .dropdown-toggle, 1107 | .cbi-tabmenu li.open.menu .menu, .cbi-tabmenu .open.dropdown .dropdown-toggle { 1108 | border-color: #999; 1109 | } 1110 | 1111 | .tabs li.open a.menu:after, .tabs .dropdown.open .dropdown-toggle:after, 1112 | .cbi-tabmenu li.open a.menu:after, .cbi-tabmenu .dropdown.open .dropdown-toggle:after { 1113 | border-top-color: #555; 1114 | } 1115 | 1116 | .tab-content > .tab-pane, 1117 | .tab-content > div { 1118 | display: none; 1119 | } 1120 | 1121 | .tab-content > .active { 1122 | display: block; 1123 | } 1124 | 1125 | .breadcrumb { 1126 | padding: 7px 14px; 1127 | margin: 0 0 18px; 1128 | background-color: #f5f5f5; 1129 | background-repeat: repeat-x; 1130 | background-image: linear-gradient(to bottom, #fff, #f5f5f5); 1131 | border: 1px solid #ddd; 1132 | border-radius: 3px; 1133 | box-shadow: inset 0 1px 0 #fff; 1134 | } 1135 | 1136 | .breadcrumb li { 1137 | display: inline; 1138 | text-shadow: 0 1px 0 #fff; 1139 | } 1140 | 1141 | .breadcrumb .divider { 1142 | padding: 0 5px; 1143 | color: #bfbfbf; 1144 | } 1145 | 1146 | .breadcrumb .active a { 1147 | color: #404040; 1148 | } 1149 | 1150 | footer { 1151 | margin-top: 17px; 1152 | padding-top: 17px; 1153 | border-top: 1px solid #eee; 1154 | } 1155 | 1156 | .btn.danger, 1157 | .alert-message.danger, 1158 | .btn.danger:hover, 1159 | .alert-message.danger:hover, 1160 | .btn.error, 1161 | .alert-message.error, 1162 | .btn.error:hover, 1163 | .alert-message.error:hover, 1164 | .btn.success, 1165 | .alert-message.success, 1166 | .btn.success:hover, 1167 | .alert-message.success:hover, 1168 | .btn.info, 1169 | .alert-message.info, 1170 | .btn.info:hover, 1171 | .alert-message.info:hover { 1172 | color: #fff; 1173 | } 1174 | 1175 | .btn .close, .alert-message .close { 1176 | font-family: Arial, sans-serif; 1177 | line-height: 18px; 1178 | } 1179 | 1180 | .btn.danger, 1181 | .alert-message.danger, 1182 | .btn.error, 1183 | .alert-message.error { 1184 | background: linear-gradient(to bottom, #ee5f5b, #c43c35) repeat-x; 1185 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 1186 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 1187 | } 1188 | 1189 | .btn.success, .alert-message.success { 1190 | background: linear-gradient(to bottom, #62c462, #57a957) repeat-x; 1191 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 1192 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 1193 | } 1194 | 1195 | .btn.info, .alert-message.info { 1196 | background: linear-gradient(to bottom, #5bc0de, #339bb9) repeat-x; 1197 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 1198 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 1199 | } 1200 | 1201 | .alert-message.notice { 1202 | background: linear-gradient(to bottom, #efefef, #fefefe) repeat-x; 1203 | text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.25); 1204 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 1205 | } 1206 | 1207 | .btn, 1208 | .cbi-button { 1209 | cursor: pointer; 1210 | display: inline-block; 1211 | background: linear-gradient(#fff, #fff 25%, #e6e6e6) no-repeat; 1212 | padding: 5px 14px 6px; 1213 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 1214 | color: #333; 1215 | font-size: 13px; 1216 | line-height: normal; 1217 | border: 1px solid #ccc; 1218 | border-bottom-color: #bbb; 1219 | border-radius: 4px; 1220 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 1221 | } 1222 | 1223 | .btn:focus, 1224 | .cbi-button:focus { 1225 | outline: 1px dotted #666; 1226 | } 1227 | 1228 | .cbi-input-invalid, 1229 | .cbi-value-error input { 1230 | color: #f00; 1231 | border-color: #f00; 1232 | } 1233 | 1234 | .cbi-button-positive, 1235 | .cbi-button-fieldadd, 1236 | .cbi-button-add, 1237 | .cbi-button-save { 1238 | border-color: #4a4; 1239 | color: #4a4; 1240 | } 1241 | 1242 | .cbi-button-neutral, 1243 | .cbi-button-download, 1244 | .cbi-button-find, 1245 | .cbi-button-link, 1246 | .cbi-button-up, 1247 | .cbi-button-down { 1248 | color: #444; 1249 | } 1250 | 1251 | .btn.primary, 1252 | .cbi-button-action, 1253 | .cbi-button-apply, 1254 | .cbi-button-reload, 1255 | .cbi-button-edit { 1256 | border-color: #0069d6; 1257 | color: #0069d6; 1258 | } 1259 | 1260 | .cbi-button-negative, 1261 | .cbi-section-remove .cbi-button, 1262 | .cbi-button-reset, 1263 | .cbi-button-remove { 1264 | border-color: #c44; 1265 | color: #c44; 1266 | } 1267 | 1268 | .cbi-page-actions::after { 1269 | display: table; 1270 | content: ""; 1271 | clear: both; 1272 | } 1273 | 1274 | .cbi-page-actions > :not([method="post"]):not(.cbi-button-apply):not(.cbi-button-save):not(.cbi-button-reset) { 1275 | float: left; 1276 | margin-right: .4em; 1277 | } 1278 | 1279 | .btn.primary, 1280 | .cbi-button-action.important, 1281 | .cbi-page-actions .cbi-button-apply, 1282 | .cbi-section-actions .cbi-button-edit { 1283 | color: #fff; 1284 | background: linear-gradient(to bottom, #0069d6, #0049d6) no-repeat; 1285 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 1286 | } 1287 | 1288 | .cbi-button-positive.important, 1289 | .cbi-page-actions .cbi-button-save { 1290 | color: #fff; 1291 | background: linear-gradient(to bottom, #4a4, #484) no-repeat; 1292 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 1293 | } 1294 | 1295 | .cbi-button-negative.important { 1296 | color: #fff; 1297 | background: linear-gradient(to bottom, #c44, #c00) no-repeat; 1298 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 1299 | } 1300 | 1301 | .cbi-page-actions .cbi-button-apply + .cbi-button-save { 1302 | background: linear-gradient(#fff, #fff 25%, #e6e6e6); 1303 | text-shadow: 0 -1px 0 rgba(255, 255, 255, 0.75); 1304 | color: #4a4; 1305 | } 1306 | 1307 | .cbi-dropdown { 1308 | border: 1px solid #ccc; 1309 | border-radius: 3px; 1310 | display: inline-flex; 1311 | padding: 0; 1312 | cursor: pointer; 1313 | height: auto; 1314 | background: linear-gradient(#fff 0%, #e9e8e6 100%); 1315 | position: relative; 1316 | color: #404040; 1317 | } 1318 | 1319 | .cbi-dropdown:focus { 1320 | outline: 2px solid #4b6e9b; 1321 | } 1322 | 1323 | .cbi-dropdown > ul { 1324 | margin: 0 !important; 1325 | padding: 0; 1326 | list-style: none; 1327 | overflow-x: hidden; 1328 | overflow-y: auto; 1329 | display: flex; 1330 | width: 100%; 1331 | } 1332 | 1333 | .cbi-dropdown > ul.preview { 1334 | display: none; 1335 | } 1336 | 1337 | .cbi-dropdown > .open, 1338 | .cbi-dropdown > .more { 1339 | flex-grow: 0; 1340 | flex-shrink: 0; 1341 | display: flex; 1342 | flex-direction: column; 1343 | justify-content: center; 1344 | text-align: center; 1345 | line-height: 2em; 1346 | padding: 0 .25em; 1347 | } 1348 | 1349 | .cbi-dropdown > .more, 1350 | .cbi-dropdown > ul > li[placeholder] { 1351 | color: #777; 1352 | font-weight: bold; 1353 | text-shadow: 1px 1px 0px #fff; 1354 | display: none; 1355 | } 1356 | 1357 | .cbi-dropdown > ul > li { 1358 | display: none; 1359 | padding: .25em; 1360 | white-space: nowrap; 1361 | overflow: hidden; 1362 | text-overflow: ellipsis; 1363 | flex-shrink: 1; 1364 | flex-grow: 1; 1365 | align-items: center; 1366 | align-self: center; 1367 | color: #404040; 1368 | min-height: 20px; 1369 | } 1370 | 1371 | .cbi-dropdown > ul > li .hide-open { display: block; display: initial; } 1372 | .cbi-dropdown > ul > li .hide-close { display: none; } 1373 | 1374 | .cbi-dropdown > ul > li[display]:not([display="0"]) { 1375 | border-left: 1px solid #ccc; 1376 | } 1377 | 1378 | .cbi-dropdown[empty] > ul { 1379 | max-width: 1px; 1380 | } 1381 | 1382 | .cbi-dropdown > ul > li > form { 1383 | display: none; 1384 | margin: 0; 1385 | padding: 0; 1386 | pointer-events: none; 1387 | } 1388 | 1389 | .cbi-dropdown > ul > li img { 1390 | vertical-align: middle; 1391 | margin-right: .25em; 1392 | } 1393 | 1394 | .cbi-dropdown > ul > li > form > input[type="checkbox"] { 1395 | margin: 0; 1396 | } 1397 | 1398 | .cbi-dropdown > ul > li input[type="text"] { 1399 | height: 20px; 1400 | } 1401 | 1402 | .cbi-dropdown[open] { 1403 | position: relative; 1404 | } 1405 | 1406 | .cbi-dropdown[open] > ul.dropdown { 1407 | display: block; 1408 | background: #f6f6f5; 1409 | border: 1px solid #918e8c; 1410 | box-shadow: 0 0 4px #918e8c; 1411 | position: absolute; 1412 | z-index: 1000; 1413 | max-width: none; 1414 | min-width: 100%; 1415 | width: auto; 1416 | } 1417 | 1418 | .cbi-dropdown > ul > li[display], 1419 | .cbi-dropdown[open] > ul.preview, 1420 | .cbi-dropdown[open] > ul.dropdown > li, 1421 | .cbi-dropdown[multiple] > ul > li > label, 1422 | .cbi-dropdown[multiple][open] > ul.dropdown > li, 1423 | .cbi-dropdown[multiple][more] > .more, 1424 | .cbi-dropdown[multiple][empty] > .more { 1425 | flex-grow: 1; 1426 | display: flex; 1427 | } 1428 | 1429 | .cbi-dropdown[empty] > ul > li, 1430 | .cbi-dropdown[optional][open] > ul.dropdown > li[placeholder], 1431 | .cbi-dropdown[multiple][open] > ul.dropdown > li > form { 1432 | display: block; 1433 | } 1434 | 1435 | .cbi-dropdown[open] > ul.dropdown > li .hide-open { display: none; } 1436 | .cbi-dropdown[open] > ul.dropdown > li .hide-close { display: block; display: initial; } 1437 | 1438 | .cbi-dropdown[open] > ul.dropdown > li { 1439 | border-bottom: 1px solid #ccc; 1440 | } 1441 | 1442 | .cbi-dropdown[open] > ul.dropdown > li[selected] { 1443 | background: #b0d0f0; 1444 | } 1445 | 1446 | .cbi-dropdown[open] > ul.dropdown > li.focus { 1447 | background: linear-gradient(90deg, #a3c2e8 0%, #84aad9 100%); 1448 | } 1449 | 1450 | .cbi-dropdown[open] > ul.dropdown > li:last-child { 1451 | margin-bottom: 0; 1452 | border-bottom: none; 1453 | } 1454 | 1455 | .cbi-dropdown[disabled] { 1456 | pointer-events: none; 1457 | opacity: .6; 1458 | } 1459 | 1460 | input[type="text"] + .cbi-button, 1461 | input[type="password"] + .cbi-button, 1462 | select + .cbi-button { 1463 | border-radius: 0 3px 3px 0; 1464 | border-color: #ccc; 1465 | margin: 0 0 1px -2px; 1466 | padding: 0 6px; 1467 | vertical-align: top; 1468 | height: 28px; 1469 | font-size: 14px; 1470 | font-weight: bold; 1471 | line-height: 28px; 1472 | } 1473 | 1474 | select + .cbi-button { 1475 | border-left-color: transparent; 1476 | } 1477 | 1478 | .cbi-title-ref { 1479 | color: #37c; 1480 | } 1481 | 1482 | .cbi-title-ref::after { 1483 | content: "➙"; 1484 | } 1485 | 1486 | .cbi-tooltip-container { 1487 | cursor: help; 1488 | } 1489 | 1490 | .cbi-tooltip { 1491 | position: absolute; 1492 | z-index: 1000; 1493 | left: -1000px; 1494 | opacity: 0; 1495 | transition: opacity .25s ease-out; 1496 | } 1497 | 1498 | .cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { 1499 | left: auto; 1500 | opacity: 1; 1501 | transition: opacity .25s ease-in; 1502 | } 1503 | 1504 | .zonebadge .cbi-tooltip { 1505 | padding: 1px; 1506 | background: inherit; 1507 | margin: -1.6em 0 0 -5px; 1508 | border-radius: 3px; 1509 | pointer-events: none; 1510 | box-shadow: 0 0 3px #444; 1511 | } 1512 | 1513 | .zonebadge .cbi-tooltip > * { 1514 | margin: 1px; 1515 | } 1516 | 1517 | .zone-forwards { 1518 | display: flex; 1519 | flex-wrap: wrap; 1520 | } 1521 | 1522 | .zone-forwards > * { 1523 | flex: 1 1 40%; 1524 | padding: 1px; 1525 | } 1526 | 1527 | .zone-forwards > span { 1528 | flex-basis: 10%; 1529 | text-align: center; 1530 | } 1531 | 1532 | .zone-forwards .zone-src, 1533 | .zone-forwards .zone-dest { 1534 | display: flex; 1535 | flex-direction: column; 1536 | } 1537 | 1538 | .btn.active, .btn:active { 1539 | box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05); 1540 | } 1541 | 1542 | .btn.disabled { 1543 | cursor: default; 1544 | background-image: none; 1545 | opacity: 0.65; 1546 | box-shadow: none; 1547 | } 1548 | 1549 | .btn[disabled] { 1550 | cursor: default; 1551 | background-image: none; 1552 | opacity: 0.65; 1553 | box-shadow: none; 1554 | } 1555 | 1556 | .btn.large { 1557 | font-size: 15px; 1558 | line-height: normal; 1559 | padding: 9px 14px 9px; 1560 | border-radius: 6px; 1561 | } 1562 | 1563 | .btn.small { 1564 | padding: 7px 9px 7px; 1565 | font-size: 11px; 1566 | } 1567 | 1568 | button.btn::-moz-focus-inner, input[type=submit].btn::-moz-focus-inner { 1569 | padding: 0; 1570 | border: 0; 1571 | } 1572 | 1573 | .close { 1574 | float: right; 1575 | color: #000; 1576 | font-size: 20px; 1577 | font-weight: bold; 1578 | line-height: 13.5px; 1579 | text-shadow: 0 1px 0 #fff; 1580 | opacity: 0.25; 1581 | } 1582 | 1583 | .close:hover { 1584 | color: #000; 1585 | text-decoration: none; 1586 | opacity: 0.4; 1587 | } 1588 | 1589 | .alert-message { 1590 | position: relative; 1591 | padding: 7px 15px; 1592 | margin-bottom: 18px; 1593 | color: #404040; 1594 | background: linear-gradient(to bottom, #fceec1, #eedc94) repeat-x; 1595 | text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); 1596 | border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); 1597 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 1598 | border-width: 1px; 1599 | border-style: solid; 1600 | border-radius: 4px; 1601 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25); 1602 | } 1603 | 1604 | .alert-message .close { 1605 | margin-top: 1px; 1606 | *margin-top: 0; 1607 | } 1608 | 1609 | .alert-message a { 1610 | font-weight: bold; 1611 | color: #404040; 1612 | } 1613 | 1614 | .alert-message.danger p a, 1615 | .alert-message.error p a, 1616 | .alert-message.success p a, 1617 | .alert-message.info p a { 1618 | color: #fff; 1619 | } 1620 | 1621 | .alert-message h5 { 1622 | line-height: 18px; 1623 | } 1624 | 1625 | .alert-message p { 1626 | margin-bottom: 0; 1627 | } 1628 | 1629 | .alert-message div { 1630 | margin-top: 5px; 1631 | margin-bottom: 2px; 1632 | line-height: 28px; 1633 | } 1634 | 1635 | .label { 1636 | padding: 1px 3px 2px; 1637 | font-size: 9.75px; 1638 | font-weight: bold; 1639 | color: #fff !important; 1640 | text-transform: uppercase; 1641 | white-space: nowrap; 1642 | background-color: #bfbfbf; 1643 | border-radius: 3px; 1644 | text-shadow: none; 1645 | } 1646 | 1647 | a.label:link, 1648 | a.label:visited { 1649 | color: #fff; 1650 | } 1651 | 1652 | a.label:hover { 1653 | text-decoration: none; 1654 | } 1655 | 1656 | .label.important { 1657 | background-color: #c43c35; 1658 | } 1659 | 1660 | .label.warning { 1661 | background-color: #f89406; 1662 | } 1663 | 1664 | .label.success { 1665 | background-color: #46a546; 1666 | } 1667 | 1668 | .label.notice { 1669 | background-color: #62cffc; 1670 | } 1671 | 1672 | /* LuCI specific items */ 1673 | .hidden { display: none } 1674 | 1675 | #memtotal > div, 1676 | #memfree > div, 1677 | #memcache > div, 1678 | #membuff > div, 1679 | #conns > div { 1680 | border: 1px solid #ccc; 1681 | border-radius: 3px 3px 3px 3px; 1682 | color: #808080; 1683 | display: inline-block; 1684 | font-size: 13px; 1685 | line-height: 18px; 1686 | } 1687 | 1688 | #xhr_poll_status { 1689 | cursor: pointer; 1690 | } 1691 | 1692 | form.inline { display: inline; margin-bottom: 0; } 1693 | 1694 | header .pull-right { padding-top: 8px; } 1695 | 1696 | #modemenu li:last-child span.divider { display: none } 1697 | 1698 | #syslog { width: 100%; } 1699 | 1700 | .cbi-section-table .tr:hover .td, 1701 | .cbi-section-table .tr:hover .th, 1702 | .cbi-section-table .tr:hover::before { 1703 | background-color: #f5f5f5; 1704 | } 1705 | 1706 | .cbi-section-table .tr.cbi-section-table-descr .th { 1707 | font-weight: normal; 1708 | } 1709 | 1710 | .cbi-section-table-titles.named::before, 1711 | .cbi-section-table-descr.named::before, 1712 | .cbi-section-table-row[data-title]::before { 1713 | content: attr(data-title) " "; 1714 | display: table-cell; 1715 | padding: 10px 10px 9px; 1716 | line-height: 18px; 1717 | font-weight: bold; 1718 | vertical-align: middle; 1719 | } 1720 | 1721 | .cbi-section-table-titles.named::before, 1722 | .cbi-section-table-descr.named::before, 1723 | .cbi-section-table-row[data-title]::before { 1724 | border-top: 1px solid #ddd; 1725 | } 1726 | 1727 | .left { text-align: left !important; } 1728 | .right { text-align: right !important; } 1729 | .center { text-align: center !important; } 1730 | .top { vertical-align: top !important; } 1731 | .middle { vertical-align: middle !important; } 1732 | .bottom { vertical-align: bottom !important; } 1733 | 1734 | .cbi-value-field { line-height: 1.5em; } 1735 | 1736 | .cbi-value-field input[type=checkbox], 1737 | .cbi-value-field input[type=radio] { 1738 | margin-top: 8px; 1739 | margin-right: 6px; 1740 | } 1741 | 1742 | table table td, 1743 | .cbi-value-field table td { 1744 | border: none; 1745 | } 1746 | 1747 | .table.cbi-section-table input[type="password"], 1748 | .table.cbi-section-table input[type="text"], 1749 | .table.cbi-section-table textarea, 1750 | .table.cbi-section-table select { 1751 | width: 100%; 1752 | } 1753 | 1754 | .table.cbi-section-table .td.cbi-section-table-cell { 1755 | white-space: nowrap; 1756 | text-align: right; 1757 | } 1758 | 1759 | .table.cbi-section-table .td.cbi-section-table-cell select { 1760 | width: inherit; 1761 | } 1762 | 1763 | .td.cbi-section-actions { 1764 | text-align: right; 1765 | vertical-align: middle; 1766 | } 1767 | 1768 | .td.cbi-section-actions > * { 1769 | display: flex; 1770 | } 1771 | 1772 | .td.cbi-section-actions > * > *, 1773 | .td.cbi-section-actions > * > form > * { 1774 | flex: 1 1 4em; 1775 | margin: 0 1px; 1776 | } 1777 | 1778 | .td.cbi-section-actions > * > form { 1779 | display: inline-flex; 1780 | margin: 0; 1781 | } 1782 | 1783 | .table.valign-middle .td { 1784 | vertical-align: middle; 1785 | } 1786 | 1787 | .cbi-rowstyle-2, 1788 | .tr.table-titles, 1789 | .tr.cbi-section-table-titles { 1790 | background: #f9f9f9; 1791 | } 1792 | 1793 | .cbi-value-description { 1794 | background-image: url(/luci-static/resources/cbi/help.gif); 1795 | background-position: .25em .2em; 1796 | background-repeat: no-repeat; 1797 | margin: .25em 0 0 0; 1798 | padding: 0 0 0 1.7em; 1799 | } 1800 | 1801 | .cbi-section-error { 1802 | border: 1px solid #f00; 1803 | border-radius: 3px; 1804 | background-color: #fce6e6; 1805 | padding: 5px; 1806 | margin-bottom: 18px; 1807 | } 1808 | 1809 | .cbi-section-error ul { margin: 0 0 0 20px; } 1810 | 1811 | .cbi-section-error ul li { 1812 | color: #f00; 1813 | font-weight: bold; 1814 | } 1815 | 1816 | .ifacebox { 1817 | background-color: #fff; 1818 | border: 1px solid #ccc; 1819 | margin: 0 10px; 1820 | text-align: center; 1821 | white-space: nowrap; 1822 | background-image: linear-gradient(#fff, #fff 25%, #f9f9f9); 1823 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 1824 | border-radius: 4px; 1825 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 1826 | display: inline-flex; 1827 | flex-direction: column; 1828 | line-height: 1.2em; 1829 | min-width: 100px; 1830 | } 1831 | 1832 | .ifacebox .ifacebox-head { 1833 | border-bottom: 1px solid #ccc; 1834 | padding: 2px; 1835 | background: #eee; 1836 | } 1837 | 1838 | .ifacebox .ifacebox-head.active { 1839 | background: #90c0e0; 1840 | } 1841 | 1842 | .ifacebox .ifacebox-body { 1843 | padding: .25em; 1844 | } 1845 | 1846 | .ifacebadge { 1847 | display: inline-block; 1848 | flex-direction: row; 1849 | white-space: nowrap; 1850 | background-color: #fff; 1851 | border: 1px solid #ccc; 1852 | padding: 2px; 1853 | background-image: linear-gradient(#fff, #fff 25%, #f9f9f9); 1854 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 1855 | border-radius: 4px; 1856 | box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); 1857 | cursor: default; 1858 | line-height: 1.2em; 1859 | } 1860 | 1861 | .ifacebadge img { 1862 | width: 16px; 1863 | height: 16px; 1864 | vertical-align: middle; 1865 | } 1866 | 1867 | .ifacebadge-active { 1868 | border-color: #000; 1869 | font-weight: bold; 1870 | } 1871 | 1872 | .network-status-table { 1873 | display: flex; 1874 | flex-wrap: wrap; 1875 | } 1876 | 1877 | .network-status-table .ifacebox { 1878 | margin: .5em; 1879 | flex-grow: 1; 1880 | } 1881 | 1882 | .network-status-table .ifacebox-body { 1883 | display: flex; 1884 | flex-direction: column; 1885 | height: 100%; 1886 | text-align: left; 1887 | } 1888 | 1889 | .network-status-table .ifacebox-body > * { 1890 | margin: .25em; 1891 | } 1892 | 1893 | .network-status-table .ifacebox-body > span { 1894 | flex: 10 10 auto; 1895 | } 1896 | 1897 | .network-status-table .ifacebox-body > div { 1898 | display: flex; 1899 | flex-wrap: wrap; 1900 | margin: -.125em; 1901 | } 1902 | 1903 | #dsl_status_table .ifacebox-body > span > strong { 1904 | display: inline-block; 1905 | min-width: 35%; 1906 | } 1907 | 1908 | .ifacebadge.large, 1909 | .network-status-table .ifacebox-body .ifacebadge { 1910 | display: inline-flex; 1911 | flex: 1; 1912 | padding: .25em; 1913 | min-width: 220px; 1914 | margin: .125em; 1915 | } 1916 | 1917 | .ifacebadge > *, 1918 | .ifacebadge.large > * { 1919 | margin: 0 .125em; 1920 | } 1921 | 1922 | .zonebadge { 1923 | padding: 2px; 1924 | border-radius: 4px; 1925 | display: inline-block; 1926 | white-space: nowrap; 1927 | color: #666; 1928 | text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); 1929 | } 1930 | 1931 | .zonebadge > em, 1932 | .zonebadge > strong { 1933 | margin: 0 2px; 1934 | display: inline-block; 1935 | } 1936 | 1937 | .zonebadge input { 1938 | width: 6em; 1939 | } 1940 | 1941 | .zonebadge > .ifacebadge { 1942 | margin-left: 2px; 1943 | } 1944 | 1945 | .zonebadge-empty { 1946 | border: 1px dashed #aaa; 1947 | color: #aaa; 1948 | font-style: italic; 1949 | font-size: smaller; 1950 | } 1951 | 1952 | div.cbi-value var, 1953 | .td.cbi-value-field var { 1954 | font-style: italic; 1955 | color: #0069d6; 1956 | } 1957 | 1958 | .uci-change-list { 1959 | font-family: monospace; 1960 | } 1961 | 1962 | .uci-change-list ins, 1963 | .uci-change-legend-label ins { 1964 | text-decoration: none; 1965 | border: 1px solid #0f0; 1966 | background-color: #cfc; 1967 | display: block; 1968 | padding: 2px; 1969 | } 1970 | 1971 | .uci-change-list del, 1972 | .uci-change-legend-label del { 1973 | text-decoration: none; 1974 | border: 1px solid #f00; 1975 | background-color: #fcc; 1976 | display: block; 1977 | font-style: normal; 1978 | padding: 2px; 1979 | } 1980 | 1981 | .uci-change-list var, 1982 | .uci-change-legend-label var { 1983 | text-decoration: none; 1984 | border: 1px solid #ccc; 1985 | background-color: #eee; 1986 | display: block; 1987 | font-style: normal; 1988 | padding: 2px; 1989 | line-height: 19px; 1990 | white-space: pre; 1991 | } 1992 | 1993 | .uci-change-list var ins, 1994 | .uci-change-list var del { 1995 | display: inline; 1996 | /*border: none;*/ 1997 | white-space: pre; 1998 | font-style: normal; 1999 | padding: 0px; 2000 | } 2001 | 2002 | .uci-change-legend { 2003 | padding: 5px; 2004 | } 2005 | 2006 | .uci-change-legend-label { 2007 | width: 150px; 2008 | float: left; 2009 | } 2010 | 2011 | .uci-change-legend-label > ins, 2012 | .uci-change-legend-label > del, 2013 | .uci-change-legend-label > var { 2014 | float: left; 2015 | margin-right: 4px; 2016 | width: 10px; 2017 | height: 10px; 2018 | display: block; 2019 | } 2020 | 2021 | .uci-change-legend-label var ins, 2022 | .uci-change-legend-label var del { 2023 | line-height: 6px; 2024 | border: none; 2025 | } 2026 | 2027 | html body.apply-overlay-active { 2028 | height: calc(100vh - 63px); 2029 | } 2030 | 2031 | #applyreboot-section { 2032 | line-height: 300%; 2033 | } 2034 | -------------------------------------------------------------------------------- /src/webSite/static/css/mobile.css: -------------------------------------------------------------------------------- 1 | header h3 a, header .brand { 2 | display:none !important; 3 | } 4 | 5 | @media screen and (max-device-width: 600px) { 6 | #maincontent.container { 7 | margin-top: 30px; 8 | } 9 | } 10 | 11 | @media screen and (max-device-width: 360px) { 12 | #maincontent.container { 13 | margin-top: 60px; 14 | } 15 | } 16 | 17 | @media screen and (max-device-width: 200px) { 18 | #maincontent.container { 19 | margin-top: 230px; 20 | } 21 | } -------------------------------------------------------------------------------- /src/webSite/static/midokure.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RyoJerryYu/autoLive/800d4e7d1e4a1843bd5594d3323ecd78bb1fff3f/src/webSite/static/midokure.ico -------------------------------------------------------------------------------- /src/webSite/static/scripts/cbi.js: -------------------------------------------------------------------------------- 1 | /* 2 | LuCI - Lua Configuration Interface 3 | 4 | Copyright 2008 Steven Barth 5 | Copyright 2008-2012 Jo-Philipp Wich 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | */ 13 | 14 | var cbi_d = []; 15 | var cbi_t = []; 16 | var cbi_strings = { path: {}, label: {} }; 17 | 18 | function Int(x) { 19 | return (/^-?\d+$/.test(x) ? +x : NaN); 20 | } 21 | 22 | function Dec(x) { 23 | return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN); 24 | } 25 | 26 | function IPv4(x) { 27 | if (!x.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) 28 | return null; 29 | 30 | if (RegExp.$1 > 255 || RegExp.$2 > 255 || RegExp.$3 > 255 || RegExp.$4 > 255) 31 | return null; 32 | 33 | return [ +RegExp.$1, +RegExp.$2, +RegExp.$3, +RegExp.$4 ]; 34 | } 35 | 36 | function IPv6(x) { 37 | if (x.match(/^([a-fA-F0-9:]+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)) { 38 | var v6 = RegExp.$1, v4 = IPv4(RegExp.$2); 39 | 40 | if (!v4) 41 | return null; 42 | 43 | x = v6 + ':' + (v4[0] * 256 + v4[1]).toString(16) 44 | + ':' + (v4[2] * 256 + v4[3]).toString(16); 45 | } 46 | 47 | if (!x.match(/^[a-fA-F0-9:]+$/)) 48 | return null; 49 | 50 | var prefix_suffix = x.split(/::/); 51 | 52 | if (prefix_suffix.length > 2) 53 | return null; 54 | 55 | var prefix = (prefix_suffix[0] || '0').split(/:/); 56 | var suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : []; 57 | 58 | if (suffix.length ? (prefix.length + suffix.length > 7) : (prefix.length > 8)) 59 | return null; 60 | 61 | var i, word; 62 | var words = []; 63 | 64 | for (i = 0, word = parseInt(prefix[0], 16); i < prefix.length; word = parseInt(prefix[++i], 16)) 65 | if (prefix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF) 66 | words.push(word); 67 | else 68 | return null; 69 | 70 | for (i = 0; i < (8 - prefix.length - suffix.length); i++) 71 | words.push(0); 72 | 73 | for (i = 0, word = parseInt(suffix[0], 16); i < suffix.length; word = parseInt(suffix[++i], 16)) 74 | if (suffix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF) 75 | words.push(word); 76 | else 77 | return null; 78 | 79 | return words; 80 | } 81 | 82 | var cbi_validators = { 83 | 84 | 'integer': function() 85 | { 86 | return !!Int(this); 87 | }, 88 | 89 | 'uinteger': function() 90 | { 91 | return (Int(this) >= 0); 92 | }, 93 | 94 | 'float': function() 95 | { 96 | return !!Dec(this); 97 | }, 98 | 99 | 'ufloat': function() 100 | { 101 | return (Dec(this) >= 0); 102 | }, 103 | 104 | 'ipaddr': function() 105 | { 106 | return cbi_validators.ip4addr.apply(this) || 107 | cbi_validators.ip6addr.apply(this); 108 | }, 109 | 110 | 'ip4addr': function() 111 | { 112 | var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})|\/(\d{1,2}))?$/); 113 | return !!(m && IPv4(m[1]) && (m[2] ? IPv4(m[2]) : (m[3] ? cbi_validators.ip4prefix.apply(m[3]) : true))); 114 | }, 115 | 116 | 'ip6addr': function() 117 | { 118 | var m = this.match(/^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/); 119 | return !!(m && IPv6(m[1]) && (m[2] ? cbi_validators.ip6prefix.apply(m[2]) : true)); 120 | }, 121 | 122 | 'ip4prefix': function() 123 | { 124 | return !isNaN(this) && this >= 0 && this <= 32; 125 | }, 126 | 127 | 'ip6prefix': function() 128 | { 129 | return !isNaN(this) && this >= 0 && this <= 128; 130 | }, 131 | 132 | 'cidr': function() 133 | { 134 | return cbi_validators.cidr4.apply(this) || 135 | cbi_validators.cidr6.apply(this); 136 | }, 137 | 138 | 'cidr4': function() 139 | { 140 | var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/); 141 | return !!(m && IPv4(m[1]) && cbi_validators.ip4prefix.apply(m[2])); 142 | }, 143 | 144 | 'cidr6': function() 145 | { 146 | var m = this.match(/^([0-9a-fA-F:.]+)\/(\d{1,3})$/); 147 | return !!(m && IPv6(m[1]) && cbi_validators.ip6prefix.apply(m[2])); 148 | }, 149 | 150 | 'ipnet4': function() 151 | { 152 | var m = this.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/); 153 | return !!(m && IPv4(m[1]) && IPv4(m[2])); 154 | }, 155 | 156 | 'ipnet6': function() 157 | { 158 | var m = this.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/); 159 | return !!(m && IPv6(m[1]) && IPv6(m[2])); 160 | }, 161 | 162 | 'ip6hostid': function() 163 | { 164 | if (this == "eui64" || this == "random") 165 | return true; 166 | 167 | var v6 = IPv6(this); 168 | return !(!v6 || v6[0] || v6[1] || v6[2] || v6[3]); 169 | }, 170 | 171 | 'ipmask': function() 172 | { 173 | return cbi_validators.ipmask4.apply(this) || 174 | cbi_validators.ipmask6.apply(this); 175 | }, 176 | 177 | 'ipmask4': function() 178 | { 179 | return cbi_validators.cidr4.apply(this) || 180 | cbi_validators.ipnet4.apply(this) || 181 | cbi_validators.ip4addr.apply(this); 182 | }, 183 | 184 | 'ipmask6': function() 185 | { 186 | return cbi_validators.cidr6.apply(this) || 187 | cbi_validators.ipnet6.apply(this) || 188 | cbi_validators.ip6addr.apply(this); 189 | }, 190 | 191 | 'port': function() 192 | { 193 | var p = Int(this); 194 | return (p >= 0 && p <= 65535); 195 | }, 196 | 197 | 'portrange': function() 198 | { 199 | if (this.match(/^(\d+)-(\d+)$/)) 200 | { 201 | var p1 = +RegExp.$1; 202 | var p2 = +RegExp.$2; 203 | return (p1 <= p2 && p2 <= 65535); 204 | } 205 | 206 | return cbi_validators.port.apply(this); 207 | }, 208 | 209 | 'macaddr': function() 210 | { 211 | return (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null); 212 | }, 213 | 214 | 'host': function(ipv4only) 215 | { 216 | return cbi_validators.hostname.apply(this) || 217 | ((ipv4only != 1) && cbi_validators.ipaddr.apply(this)) || 218 | ((ipv4only == 1) && cbi_validators.ip4addr.apply(this)); 219 | }, 220 | 221 | 'hostname': function(strict) 222 | { 223 | if (this.length <= 253) 224 | return (this.match(/^[a-zA-Z0-9_]+$/) != null || 225 | (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) && 226 | this.match(/[^0-9.]/))) && 227 | (!strict || !this.match(/^_/)); 228 | 229 | return false; 230 | }, 231 | 232 | 'network': function() 233 | { 234 | return cbi_validators.uciname.apply(this) || 235 | cbi_validators.host.apply(this); 236 | }, 237 | 238 | 'hostport': function(ipv4only) 239 | { 240 | var hp = this.split(/:/); 241 | 242 | if (hp.length == 2) 243 | return (cbi_validators.host.apply(hp[0], ipv4only) && 244 | cbi_validators.port.apply(hp[1])); 245 | 246 | return false; 247 | }, 248 | 249 | 'ip4addrport': function() 250 | { 251 | var hp = this.split(/:/); 252 | 253 | if (hp.length == 2) 254 | return (cbi_validators.ipaddr.apply(hp[0]) && 255 | cbi_validators.port.apply(hp[1])); 256 | return false; 257 | }, 258 | 259 | 'ipaddrport': function(bracket) 260 | { 261 | if (this.match(/^([^\[\]:]+):([^:]+)$/)) { 262 | var addr = RegExp.$1 263 | var port = RegExp.$2 264 | return (cbi_validators.ip4addr.apply(addr) && 265 | cbi_validators.port.apply(port)); 266 | } else if ((bracket == 1) && (this.match(/^\[(.+)\]:([^:]+)$/))) { 267 | var addr = RegExp.$1 268 | var port = RegExp.$2 269 | return (cbi_validators.ip6addr.apply(addr) && 270 | cbi_validators.port.apply(port)); 271 | } else if ((bracket != 1) && (this.match(/^([^\[\]]+):([^:]+)$/))) { 272 | var addr = RegExp.$1 273 | var port = RegExp.$2 274 | return (cbi_validators.ip6addr.apply(addr) && 275 | cbi_validators.port.apply(port)); 276 | } else { 277 | return false; 278 | } 279 | }, 280 | 281 | 'wpakey': function() 282 | { 283 | var v = this; 284 | 285 | if( v.length == 64 ) 286 | return (v.match(/^[a-fA-F0-9]{64}$/) != null); 287 | else 288 | return (v.length >= 8) && (v.length <= 63); 289 | }, 290 | 291 | 'wepkey': function() 292 | { 293 | var v = this; 294 | 295 | if ( v.substr(0,2) == 's:' ) 296 | v = v.substr(2); 297 | 298 | if( (v.length == 10) || (v.length == 26) ) 299 | return (v.match(/^[a-fA-F0-9]{10,26}$/) != null); 300 | else 301 | return (v.length == 5) || (v.length == 13); 302 | }, 303 | 304 | 'uciname': function() 305 | { 306 | return (this.match(/^[a-zA-Z0-9_]+$/) != null); 307 | }, 308 | 309 | 'range': function(min, max) 310 | { 311 | var val = Dec(this); 312 | return (val >= +min && val <= +max); 313 | }, 314 | 315 | 'min': function(min) 316 | { 317 | return (Dec(this) >= +min); 318 | }, 319 | 320 | 'max': function(max) 321 | { 322 | return (Dec(this) <= +max); 323 | }, 324 | 325 | 'rangelength': function(min, max) 326 | { 327 | var val = '' + this; 328 | return ((val.length >= +min) && (val.length <= +max)); 329 | }, 330 | 331 | 'minlength': function(min) 332 | { 333 | return ((''+this).length >= +min); 334 | }, 335 | 336 | 'maxlength': function(max) 337 | { 338 | return ((''+this).length <= +max); 339 | }, 340 | 341 | 'or': function() 342 | { 343 | for (var i = 0; i < arguments.length; i += 2) 344 | { 345 | if (typeof arguments[i] != 'function') 346 | { 347 | if (arguments[i] == this) 348 | return true; 349 | i--; 350 | } 351 | else if (arguments[i].apply(this, arguments[i+1])) 352 | { 353 | return true; 354 | } 355 | } 356 | return false; 357 | }, 358 | 359 | 'and': function() 360 | { 361 | for (var i = 0; i < arguments.length; i += 2) 362 | { 363 | if (typeof arguments[i] != 'function') 364 | { 365 | if (arguments[i] != this) 366 | return false; 367 | i--; 368 | } 369 | else if (!arguments[i].apply(this, arguments[i+1])) 370 | { 371 | return false; 372 | } 373 | } 374 | return true; 375 | }, 376 | 377 | 'neg': function() 378 | { 379 | return cbi_validators.or.apply( 380 | this.replace(/^[ \t]*![ \t]*/, ''), arguments); 381 | }, 382 | 383 | 'list': function(subvalidator, subargs) 384 | { 385 | if (typeof subvalidator != 'function') 386 | return false; 387 | 388 | var tokens = this.match(/[^ \t]+/g); 389 | for (var i = 0; i < tokens.length; i++) 390 | if (!subvalidator.apply(tokens[i], subargs)) 391 | return false; 392 | 393 | return true; 394 | }, 395 | 'phonedigit': function() 396 | { 397 | return (this.match(/^[0-9\*#!\.]+$/) != null); 398 | }, 399 | 'timehhmmss': function() 400 | { 401 | return (this.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/) != null); 402 | }, 403 | 'dateyyyymmdd': function() 404 | { 405 | if (this == null) { 406 | return false; 407 | } 408 | if (this.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) { 409 | var year = RegExp.$1; 410 | var month = RegExp.$2; 411 | var day = RegExp.$2 412 | 413 | var days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30 , 31, 30, 31 ]; 414 | function is_leap_year(year) { 415 | return ((year % 4) == 0) && ((year % 100) != 0) || ((year % 400) == 0); 416 | } 417 | function get_days_in_month(month, year) { 418 | if ((month == 2) && is_leap_year(year)) { 419 | return 29; 420 | } else { 421 | return days_in_month[month]; 422 | } 423 | } 424 | /* Firewall rules in the past don't make sense */ 425 | if (year < 2015) { 426 | return false; 427 | } 428 | if ((month <= 0) || (month > 12)) { 429 | return false; 430 | } 431 | if ((day <= 0) || (day > get_days_in_month(month, year))) { 432 | return false; 433 | } 434 | return true; 435 | 436 | } else { 437 | return false; 438 | } 439 | } 440 | }; 441 | 442 | 443 | function cbi_d_add(field, dep, index) { 444 | var obj = (typeof(field) === 'string') ? document.getElementById(field) : field; 445 | if (obj) { 446 | var entry 447 | for (var i=0; i entry.index) { 519 | break; 520 | } 521 | } 522 | 523 | if (!next) { 524 | parent.appendChild(entry.node); 525 | } else { 526 | parent.insertBefore(entry.node, next); 527 | } 528 | 529 | state = true; 530 | } 531 | 532 | // hide optionals widget if no choices remaining 533 | if (parent && parent.parentNode && parent.getAttribute('data-optionals')) 534 | parent.parentNode.style.display = (parent.options.length <= 1) ? 'none' : ''; 535 | } 536 | 537 | if (entry && entry.parent) { 538 | if (!cbi_t_update()) 539 | cbi_tag_last(parent); 540 | } 541 | 542 | if (state) { 543 | cbi_d_update(); 544 | } 545 | } 546 | 547 | function cbi_init() { 548 | var nodes; 549 | 550 | nodes = document.querySelectorAll('[data-strings]'); 551 | 552 | for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { 553 | var str = JSON.parse(node.getAttribute('data-strings')); 554 | for (var key in str) { 555 | for (var key2 in str[key]) { 556 | var dst = cbi_strings[key] || (cbi_strings[key] = { }); 557 | dst[key2] = str[key][key2]; 558 | } 559 | } 560 | } 561 | 562 | nodes = document.querySelectorAll('[data-depends]'); 563 | 564 | for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { 565 | var index = parseInt(node.getAttribute('data-index'), 10); 566 | var depends = JSON.parse(node.getAttribute('data-depends')); 567 | if (!isNaN(index) && depends.length > 0) { 568 | for (var alt = 0; alt < depends.length; alt++) { 569 | cbi_d_add(node, depends[alt], index); 570 | } 571 | } 572 | } 573 | 574 | nodes = document.querySelectorAll('[data-update]'); 575 | 576 | for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { 577 | var events = node.getAttribute('data-update').split(' '); 578 | for (var j = 0, event; (event = events[j]) !== undefined; j++) { 579 | cbi_bind(node, event, cbi_d_update); 580 | } 581 | } 582 | 583 | nodes = document.querySelectorAll('[data-choices]'); 584 | 585 | for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { 586 | var choices = JSON.parse(node.getAttribute('data-choices')); 587 | var options = {}; 588 | 589 | for (var j = 0; j < choices[0].length; j++) 590 | options[choices[0][j]] = choices[1][j]; 591 | 592 | var def = (node.getAttribute('data-optional') === 'true') 593 | ? node.placeholder || '' : null; 594 | 595 | cbi_combobox_init(node, options, def, 596 | node.getAttribute('data-manual')); 597 | } 598 | 599 | nodes = document.querySelectorAll('[data-dynlist]'); 600 | 601 | for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { 602 | var choices = JSON.parse(node.getAttribute('data-dynlist')); 603 | var options = null; 604 | 605 | if (choices[0] && choices[0].length) { 606 | options = {}; 607 | 608 | for (var j = 0; j < choices[0].length; j++) 609 | options[choices[0][j]] = choices[1][j]; 610 | } 611 | 612 | cbi_dynlist_init(node, choices[2], choices[3], options); 613 | } 614 | 615 | nodes = document.querySelectorAll('[data-type]'); 616 | 617 | for (var i = 0, node; (node = nodes[i]) !== undefined; i++) { 618 | cbi_validate_field(node, node.getAttribute('data-optional') === 'true', 619 | node.getAttribute('data-type')); 620 | } 621 | 622 | document.querySelectorAll('.cbi-dropdown').forEach(function(s) { 623 | cbi_dropdown_init(s); 624 | }); 625 | 626 | document.querySelectorAll('.cbi-tooltip:not(:empty)').forEach(function(s) { 627 | s.parentNode.classList.add('cbi-tooltip-container'); 628 | }); 629 | 630 | document.querySelectorAll('.cbi-section-remove > input[name^="cbi.rts"]').forEach(function(i) { 631 | var handler = function(ev) { 632 | var bits = this.name.split(/\./), 633 | section = document.getElementById('cbi-' + bits[2] + '-' + bits[3]); 634 | 635 | section.style.opacity = (ev.type === 'mouseover') ? 0.5 : ''; 636 | }; 637 | 638 | i.addEventListener('mouseover', handler); 639 | i.addEventListener('mouseout', handler); 640 | }); 641 | 642 | cbi_d_update(); 643 | } 644 | 645 | function cbi_bind(obj, type, callback, mode) { 646 | if (!obj.addEventListener) { 647 | obj.attachEvent('on' + type, 648 | function(){ 649 | var e = window.event; 650 | 651 | if (!e.target && e.srcElement) 652 | e.target = e.srcElement; 653 | 654 | return !!callback(e); 655 | } 656 | ); 657 | } else { 658 | obj.addEventListener(type, callback, !!mode); 659 | } 660 | return obj; 661 | } 662 | 663 | function cbi_combobox(id, values, def, man, focus) { 664 | var selid = "cbi.combobox." + id; 665 | if (document.getElementById(selid)) { 666 | return 667 | } 668 | 669 | var obj = document.getElementById(id) 670 | var sel = document.createElement("select"); 671 | sel.id = selid; 672 | sel.index = obj.index; 673 | sel.className = obj.className.replace(/cbi-input-text/, 'cbi-input-select'); 674 | 675 | if (obj.nextSibling) { 676 | obj.parentNode.insertBefore(sel, obj.nextSibling); 677 | } else { 678 | obj.parentNode.appendChild(sel); 679 | } 680 | 681 | var dt = obj.getAttribute('cbi_datatype'); 682 | var op = obj.getAttribute('cbi_optional'); 683 | 684 | if (!values[obj.value]) { 685 | if (obj.value == "") { 686 | var optdef = document.createElement("option"); 687 | optdef.value = ""; 688 | optdef.appendChild(document.createTextNode(typeof(def) === 'string' ? def : cbi_strings.label.choose)); 689 | sel.appendChild(optdef); 690 | } else { 691 | var opt = document.createElement("option"); 692 | opt.value = obj.value; 693 | opt.selected = "selected"; 694 | opt.appendChild(document.createTextNode(obj.value)); 695 | sel.appendChild(opt); 696 | } 697 | } 698 | 699 | for (var i in values) { 700 | var opt = document.createElement("option"); 701 | opt.value = i; 702 | 703 | if (obj.value == i) { 704 | opt.selected = "selected"; 705 | } 706 | 707 | opt.appendChild(document.createTextNode(values[i])); 708 | sel.appendChild(opt); 709 | } 710 | 711 | var optman = document.createElement("option"); 712 | optman.value = ""; 713 | optman.appendChild(document.createTextNode(typeof(man) === 'string' ? man : cbi_strings.label.custom)); 714 | sel.appendChild(optman); 715 | 716 | obj.style.display = "none"; 717 | 718 | if (dt) 719 | cbi_validate_field(sel, op == 'true', dt); 720 | 721 | cbi_bind(sel, "change", function() { 722 | if (sel.selectedIndex == sel.options.length - 1) { 723 | obj.style.display = "inline"; 724 | sel.blur(); 725 | sel.parentNode.removeChild(sel); 726 | obj.focus(); 727 | } else { 728 | obj.value = sel.options[sel.selectedIndex].value; 729 | } 730 | 731 | try { 732 | cbi_d_update(); 733 | } catch (e) { 734 | //Do nothing 735 | } 736 | }) 737 | 738 | // Retrigger validation in select 739 | if (focus) { 740 | sel.focus(); 741 | sel.blur(); 742 | } 743 | } 744 | 745 | function cbi_combobox_init(id, values, def, man) { 746 | var obj = (typeof(id) === 'string') ? document.getElementById(id) : id; 747 | cbi_bind(obj, "blur", function() { 748 | cbi_combobox(obj.id, values, def, man, true); 749 | }); 750 | cbi_combobox(obj.id, values, def, man, false); 751 | } 752 | 753 | function cbi_filebrowser(id, defpath) { 754 | var field = document.getElementById(id); 755 | var browser = window.open( 756 | cbi_strings.path.browser + ( field.value || defpath || '' ) + '?field=' + id, 757 | "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes" 758 | ); 759 | 760 | browser.focus(); 761 | } 762 | 763 | function cbi_browser_init(id, resource, defpath) 764 | { 765 | function cbi_browser_btnclick(e) { 766 | cbi_filebrowser(id, defpath); 767 | return false; 768 | } 769 | 770 | var field = document.getElementById(id); 771 | 772 | var btn = document.createElement('img'); 773 | btn.className = 'cbi-image-button'; 774 | btn.src = (resource || cbi_strings.path.resource) + '/cbi/folder.gif'; 775 | field.parentNode.insertBefore(btn, field.nextSibling); 776 | 777 | cbi_bind(btn, 'click', cbi_browser_btnclick); 778 | } 779 | 780 | function cbi_dynlist_init(parent, datatype, optional, choices) 781 | { 782 | var prefix = parent.getAttribute('data-prefix'); 783 | var holder = parent.getAttribute('data-placeholder'); 784 | 785 | var values; 786 | 787 | function cbi_dynlist_redraw(focus, add, del) 788 | { 789 | values = [ ]; 790 | 791 | while (parent.firstChild) 792 | { 793 | var n = parent.firstChild; 794 | var i = +n.index; 795 | 796 | if (i != del) 797 | { 798 | if (n.nodeName.toLowerCase() == 'input') 799 | values.push(n.value || ''); 800 | else if (n.nodeName.toLowerCase() == 'select') 801 | values[values.length-1] = n.options[n.selectedIndex].value; 802 | } 803 | 804 | parent.removeChild(n); 805 | } 806 | 807 | if (add >= 0) 808 | { 809 | focus = add+1; 810 | values.splice(focus, 0, ''); 811 | } 812 | else if (values.length == 0) 813 | { 814 | focus = 0; 815 | values.push(''); 816 | } 817 | 818 | for (var i = 0; i < values.length; i++) 819 | { 820 | var t = document.createElement('input'); 821 | t.id = prefix + '.' + (i+1); 822 | t.name = prefix; 823 | t.value = values[i]; 824 | t.type = 'text'; 825 | t.index = i; 826 | t.className = 'cbi-input-text'; 827 | 828 | if (i == 0 && holder) 829 | { 830 | t.placeholder = holder; 831 | } 832 | 833 | var b = E('div', { 834 | class: 'cbi-button cbi-button-' + ((i+1) < values.length ? 'remove' : 'add') 835 | }, (i+1) < values.length ? '×' : '+'); 836 | 837 | parent.appendChild(t); 838 | parent.appendChild(b); 839 | if (datatype == 'file') 840 | { 841 | cbi_browser_init(t.id, null, parent.getAttribute('data-browser-path')); 842 | } 843 | 844 | parent.appendChild(document.createElement('br')); 845 | 846 | if (datatype) 847 | { 848 | cbi_validate_field(t.id, ((i+1) == values.length) || optional, datatype); 849 | } 850 | 851 | if (choices) 852 | { 853 | cbi_combobox_init(t.id, choices, '', cbi_strings.label.custom); 854 | b.index = i; 855 | 856 | cbi_bind(b, 'keydown', cbi_dynlist_keydown); 857 | cbi_bind(b, 'keypress', cbi_dynlist_keypress); 858 | 859 | if (i == focus || -i == focus) 860 | b.focus(); 861 | } 862 | else 863 | { 864 | cbi_bind(t, 'keydown', cbi_dynlist_keydown); 865 | cbi_bind(t, 'keypress', cbi_dynlist_keypress); 866 | 867 | if (i == focus) 868 | { 869 | t.focus(); 870 | } 871 | else if (-i == focus) 872 | { 873 | t.focus(); 874 | 875 | /* force cursor to end */ 876 | var v = t.value; 877 | t.value = ' ' 878 | t.value = v; 879 | } 880 | } 881 | 882 | cbi_bind(b, 'click', cbi_dynlist_btnclick); 883 | } 884 | } 885 | 886 | function cbi_dynlist_keypress(ev) 887 | { 888 | ev = ev ? ev : window.event; 889 | 890 | var se = ev.target ? ev.target : ev.srcElement; 891 | 892 | if (se.nodeType == 3) 893 | se = se.parentNode; 894 | 895 | switch (ev.keyCode) 896 | { 897 | /* backspace, delete */ 898 | case 8: 899 | case 46: 900 | if (se.value.length == 0) 901 | { 902 | if (ev.preventDefault) 903 | ev.preventDefault(); 904 | 905 | return false; 906 | } 907 | 908 | return true; 909 | 910 | /* enter, arrow up, arrow down */ 911 | case 13: 912 | case 38: 913 | case 40: 914 | if (ev.preventDefault) 915 | ev.preventDefault(); 916 | 917 | return false; 918 | } 919 | 920 | return true; 921 | } 922 | 923 | function cbi_dynlist_keydown(ev) 924 | { 925 | ev = ev ? ev : window.event; 926 | 927 | var se = ev.target ? ev.target : ev.srcElement; 928 | 929 | if (se.nodeType == 3) 930 | se = se.parentNode; 931 | 932 | var prev = se.previousSibling; 933 | while (prev && prev.name != prefix) 934 | prev = prev.previousSibling; 935 | 936 | var next = se.nextSibling; 937 | while (next && next.name != prefix) 938 | next = next.nextSibling; 939 | 940 | /* advance one further in combobox case */ 941 | if (next && next.nextSibling.name == prefix) 942 | next = next.nextSibling; 943 | 944 | switch (ev.keyCode) 945 | { 946 | /* backspace, delete */ 947 | case 8: 948 | case 46: 949 | var del = (se.nodeName.toLowerCase() == 'select') 950 | ? true : (se.value.length == 0); 951 | 952 | if (del) 953 | { 954 | if (ev.preventDefault) 955 | ev.preventDefault(); 956 | 957 | var focus = se.index; 958 | if (ev.keyCode == 8) 959 | focus = -focus+1; 960 | 961 | cbi_dynlist_redraw(focus, -1, se.index); 962 | 963 | return false; 964 | } 965 | 966 | break; 967 | 968 | /* enter */ 969 | case 13: 970 | cbi_dynlist_redraw(-1, se.index, -1); 971 | break; 972 | 973 | /* arrow up */ 974 | case 38: 975 | if (prev) 976 | prev.focus(); 977 | 978 | break; 979 | 980 | /* arrow down */ 981 | case 40: 982 | if (next) 983 | next.focus(); 984 | 985 | break; 986 | } 987 | 988 | return true; 989 | } 990 | 991 | function cbi_dynlist_btnclick(ev) 992 | { 993 | ev = ev ? ev : window.event; 994 | 995 | var se = ev.target ? ev.target : ev.srcElement; 996 | var input = se.previousSibling; 997 | while (input && input.name != prefix) { 998 | input = input.previousSibling; 999 | } 1000 | 1001 | if (se.classList.contains('cbi-button-remove')) { 1002 | input.value = ''; 1003 | 1004 | cbi_dynlist_keydown({ 1005 | target: input, 1006 | keyCode: 8 1007 | }); 1008 | } 1009 | else { 1010 | cbi_dynlist_keydown({ 1011 | target: input, 1012 | keyCode: 13 1013 | }); 1014 | } 1015 | 1016 | return false; 1017 | } 1018 | 1019 | cbi_dynlist_redraw(NaN, -1, -1); 1020 | } 1021 | 1022 | 1023 | function cbi_t_add(section, tab) { 1024 | var t = document.getElementById('tab.' + section + '.' + tab); 1025 | var c = document.getElementById('container.' + section + '.' + tab); 1026 | 1027 | if( t && c ) { 1028 | cbi_t[section] = (cbi_t[section] || [ ]); 1029 | cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id }; 1030 | } 1031 | } 1032 | 1033 | function cbi_t_switch(section, tab) { 1034 | if( cbi_t[section] && cbi_t[section][tab] ) { 1035 | var o = cbi_t[section][tab]; 1036 | var h = document.getElementById('tab.' + section); 1037 | for( var tid in cbi_t[section] ) { 1038 | var o2 = cbi_t[section][tid]; 1039 | if( o.tab.id != o2.tab.id ) { 1040 | o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled "); 1041 | o2.container.style.display = 'none'; 1042 | } 1043 | else { 1044 | if(h) h.value = tab; 1045 | o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab "); 1046 | o2.container.style.display = 'block'; 1047 | } 1048 | } 1049 | } 1050 | return false 1051 | } 1052 | 1053 | function cbi_t_update() { 1054 | var hl_tabs = [ ]; 1055 | var updated = false; 1056 | 1057 | for( var sid in cbi_t ) 1058 | for( var tid in cbi_t[sid] ) 1059 | { 1060 | var t = cbi_t[sid][tid].tab; 1061 | var c = cbi_t[sid][tid].container; 1062 | 1063 | if (!c.firstElementChild) { 1064 | t.style.display = 'none'; 1065 | } 1066 | else if (t.style.display == 'none') { 1067 | t.style.display = ''; 1068 | t.className += ' cbi-tab-highlighted'; 1069 | hl_tabs.push(t); 1070 | } 1071 | 1072 | cbi_tag_last(c); 1073 | updated = true; 1074 | } 1075 | 1076 | if (hl_tabs.length > 0) 1077 | window.setTimeout(function() { 1078 | for( var i = 0; i < hl_tabs.length; i++ ) 1079 | hl_tabs[i].className = hl_tabs[i].className.replace(/ cbi-tab-highlighted/g, ''); 1080 | }, 750); 1081 | 1082 | return updated; 1083 | } 1084 | 1085 | 1086 | function cbi_validate_form(form, errmsg) 1087 | { 1088 | /* if triggered by a section removal or addition, don't validate */ 1089 | if( form.cbi_state == 'add-section' || form.cbi_state == 'del-section' ) 1090 | return true; 1091 | 1092 | if( form.cbi_validators ) 1093 | { 1094 | for( var i = 0; i < form.cbi_validators.length; i++ ) 1095 | { 1096 | var validator = form.cbi_validators[i]; 1097 | if( !validator() && errmsg ) 1098 | { 1099 | alert(errmsg); 1100 | return false; 1101 | } 1102 | } 1103 | } 1104 | 1105 | return true; 1106 | } 1107 | 1108 | function cbi_validate_reset(form) 1109 | { 1110 | window.setTimeout( 1111 | function() { cbi_validate_form(form, null) }, 100 1112 | ); 1113 | 1114 | return true; 1115 | } 1116 | 1117 | function cbi_validate_compile(code) 1118 | { 1119 | var pos = 0; 1120 | var esc = false; 1121 | var depth = 0; 1122 | var stack = [ ]; 1123 | 1124 | code += ','; 1125 | 1126 | for (var i = 0; i < code.length; i++) 1127 | { 1128 | if (esc) 1129 | { 1130 | esc = false; 1131 | continue; 1132 | } 1133 | 1134 | switch (code.charCodeAt(i)) 1135 | { 1136 | case 92: 1137 | esc = true; 1138 | break; 1139 | 1140 | case 40: 1141 | case 44: 1142 | if (depth <= 0) 1143 | { 1144 | if (pos < i) 1145 | { 1146 | var label = code.substring(pos, i); 1147 | label = label.replace(/\\(.)/g, '$1'); 1148 | label = label.replace(/^[ \t]+/g, ''); 1149 | label = label.replace(/[ \t]+$/g, ''); 1150 | 1151 | if (label && !isNaN(label)) 1152 | { 1153 | stack.push(parseFloat(label)); 1154 | } 1155 | else if (label.match(/^(['"]).*\1$/)) 1156 | { 1157 | stack.push(label.replace(/^(['"])(.*)\1$/, '$2')); 1158 | } 1159 | else if (typeof cbi_validators[label] == 'function') 1160 | { 1161 | stack.push(cbi_validators[label]); 1162 | stack.push(null); 1163 | } 1164 | else 1165 | { 1166 | throw "Syntax error, unhandled token '"+label+"'"; 1167 | } 1168 | } 1169 | pos = i+1; 1170 | } 1171 | depth += (code.charCodeAt(i) == 40); 1172 | break; 1173 | 1174 | case 41: 1175 | if (--depth <= 0) 1176 | { 1177 | if (typeof stack[stack.length-2] != 'function') 1178 | throw "Syntax error, argument list follows non-function"; 1179 | 1180 | stack[stack.length-1] = 1181 | arguments.callee(code.substring(pos, i)); 1182 | 1183 | pos = i+1; 1184 | } 1185 | break; 1186 | } 1187 | } 1188 | 1189 | return stack; 1190 | } 1191 | 1192 | function cbi_validate_field(cbid, optional, type) 1193 | { 1194 | var field = (typeof cbid == "string") ? document.getElementById(cbid) : cbid; 1195 | var vstack; try { vstack = cbi_validate_compile(type); } catch(e) { }; 1196 | 1197 | if (field && vstack && typeof vstack[0] == "function") 1198 | { 1199 | var validator = function() 1200 | { 1201 | // is not detached 1202 | if( field.form ) 1203 | { 1204 | field.className = field.className.replace(/ cbi-input-invalid/g, ''); 1205 | 1206 | // validate value 1207 | var value = (field.options && field.options.selectedIndex > -1) 1208 | ? field.options[field.options.selectedIndex].value : field.value; 1209 | 1210 | if (!(((value.length == 0) && optional) || vstack[0].apply(value, vstack[1]))) 1211 | { 1212 | // invalid 1213 | field.className += ' cbi-input-invalid'; 1214 | return false; 1215 | } 1216 | } 1217 | 1218 | return true; 1219 | }; 1220 | 1221 | if( ! field.form.cbi_validators ) 1222 | field.form.cbi_validators = [ ]; 1223 | 1224 | field.form.cbi_validators.push(validator); 1225 | 1226 | cbi_bind(field, "blur", validator); 1227 | cbi_bind(field, "keyup", validator); 1228 | 1229 | if (field.nodeName == 'SELECT') 1230 | { 1231 | cbi_bind(field, "change", validator); 1232 | cbi_bind(field, "click", validator); 1233 | } 1234 | 1235 | field.setAttribute("cbi_validate", validator); 1236 | field.setAttribute("cbi_datatype", type); 1237 | field.setAttribute("cbi_optional", (!!optional).toString()); 1238 | 1239 | validator(); 1240 | 1241 | var fcbox = document.getElementById('cbi.combobox.' + field.id); 1242 | if (fcbox) 1243 | cbi_validate_field(fcbox, optional, type); 1244 | } 1245 | } 1246 | 1247 | function cbi_row_swap(elem, up, store) 1248 | { 1249 | var tr = findParent(elem.parentNode, '.cbi-section-table-row'); 1250 | 1251 | if (!tr) 1252 | return false; 1253 | 1254 | tr.classList.remove('flash'); 1255 | 1256 | if (up) { 1257 | var prev = tr.previousElementSibling; 1258 | 1259 | if (prev && prev.classList.contains('cbi-section-table-row')) 1260 | tr.parentNode.insertBefore(tr, prev); 1261 | else 1262 | return; 1263 | } 1264 | else { 1265 | var next = tr.nextElementSibling ? tr.nextElementSibling.nextElementSibling : null; 1266 | 1267 | if (next && next.classList.contains('cbi-section-table-row')) 1268 | tr.parentNode.insertBefore(tr, next); 1269 | else if (!next) 1270 | tr.parentNode.appendChild(tr); 1271 | else 1272 | return; 1273 | } 1274 | 1275 | var ids = [ ]; 1276 | 1277 | for (var i = 0, n = 0; i < tr.parentNode.childNodes.length; i++) { 1278 | var node = tr.parentNode.childNodes[i]; 1279 | if (node.classList && node.classList.contains('cbi-section-table-row')) { 1280 | node.classList.remove('cbi-rowstyle-1'); 1281 | node.classList.remove('cbi-rowstyle-2'); 1282 | node.classList.add((n++ % 2) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1'); 1283 | 1284 | if (/-([^\-]+)$/.test(node.id)) 1285 | ids.push(RegExp.$1); 1286 | } 1287 | } 1288 | 1289 | var input = document.getElementById(store); 1290 | if (input) 1291 | input.value = ids.join(' '); 1292 | 1293 | window.scrollTo(0, tr.offsetTop); 1294 | window.setTimeout(function() { tr.classList.add('flash'); }, 1); 1295 | 1296 | return false; 1297 | } 1298 | 1299 | function cbi_tag_last(container) 1300 | { 1301 | var last; 1302 | 1303 | for (var i = 0; i < container.childNodes.length; i++) 1304 | { 1305 | var c = container.childNodes[i]; 1306 | if (c.nodeType == 1 && c.nodeName.toLowerCase() == 'div') 1307 | { 1308 | c.className = c.className.replace(/ cbi-value-last$/, ''); 1309 | last = c; 1310 | } 1311 | } 1312 | 1313 | if (last) 1314 | { 1315 | last.className += ' cbi-value-last'; 1316 | } 1317 | } 1318 | 1319 | function cbi_submit(elem, name, value, action) 1320 | { 1321 | var form = elem.form || findParent(elem, 'form'); 1322 | 1323 | if (!form) 1324 | return false; 1325 | 1326 | if (action) 1327 | form.action = action; 1328 | 1329 | if (name) { 1330 | var hidden = form.querySelector('input[type="hidden"][name="%s"]'.format(name)) || 1331 | E('input', { type: 'hidden', name: name }); 1332 | 1333 | hidden.value = value || '1'; 1334 | form.appendChild(hidden); 1335 | } 1336 | 1337 | form.submit(); 1338 | return true; 1339 | } 1340 | 1341 | String.prototype.format = function() 1342 | { 1343 | if (!RegExp) 1344 | return; 1345 | 1346 | var html_esc = [/&/g, '&', /"/g, '"', /'/g, ''', //g, '>']; 1347 | var quot_esc = [/"/g, '"', /'/g, ''']; 1348 | 1349 | function esc(s, r) { 1350 | if (typeof(s) !== 'string' && !(s instanceof String)) 1351 | return ''; 1352 | 1353 | for( var i = 0; i < r.length; i += 2 ) 1354 | s = s.replace(r[i], r[i+1]); 1355 | return s; 1356 | } 1357 | 1358 | var str = this; 1359 | var out = ''; 1360 | var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t|m))/; 1361 | var a = b = [], numSubstitutions = 0, numMatches = 0; 1362 | 1363 | while (a = re.exec(str)) 1364 | { 1365 | var m = a[1]; 1366 | var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5]; 1367 | var pPrecision = a[6], pType = a[7]; 1368 | 1369 | numMatches++; 1370 | 1371 | if (pType == '%') 1372 | { 1373 | subst = '%'; 1374 | } 1375 | else 1376 | { 1377 | if (numSubstitutions < arguments.length) 1378 | { 1379 | var param = arguments[numSubstitutions++]; 1380 | 1381 | var pad = ''; 1382 | if (pPad && pPad.substr(0,1) == "'") 1383 | pad = leftpart.substr(1,1); 1384 | else if (pPad) 1385 | pad = pPad; 1386 | else 1387 | pad = ' '; 1388 | 1389 | var justifyRight = true; 1390 | if (pJustify && pJustify === "-") 1391 | justifyRight = false; 1392 | 1393 | var minLength = -1; 1394 | if (pMinLength) 1395 | minLength = +pMinLength; 1396 | 1397 | var precision = -1; 1398 | if (pPrecision && pType == 'f') 1399 | precision = +pPrecision.substring(1); 1400 | 1401 | var subst = param; 1402 | 1403 | switch(pType) 1404 | { 1405 | case 'b': 1406 | subst = (+param || 0).toString(2); 1407 | break; 1408 | 1409 | case 'c': 1410 | subst = String.fromCharCode(+param || 0); 1411 | break; 1412 | 1413 | case 'd': 1414 | subst = ~~(+param || 0); 1415 | break; 1416 | 1417 | case 'u': 1418 | subst = ~~Math.abs(+param || 0); 1419 | break; 1420 | 1421 | case 'f': 1422 | subst = (precision > -1) 1423 | ? ((+param || 0.0)).toFixed(precision) 1424 | : (+param || 0.0); 1425 | break; 1426 | 1427 | case 'o': 1428 | subst = (+param || 0).toString(8); 1429 | break; 1430 | 1431 | case 's': 1432 | subst = param; 1433 | break; 1434 | 1435 | case 'x': 1436 | subst = ('' + (+param || 0).toString(16)).toLowerCase(); 1437 | break; 1438 | 1439 | case 'X': 1440 | subst = ('' + (+param || 0).toString(16)).toUpperCase(); 1441 | break; 1442 | 1443 | case 'h': 1444 | subst = esc(param, html_esc); 1445 | break; 1446 | 1447 | case 'q': 1448 | subst = esc(param, quot_esc); 1449 | break; 1450 | 1451 | case 't': 1452 | var td = 0; 1453 | var th = 0; 1454 | var tm = 0; 1455 | var ts = (param || 0); 1456 | 1457 | if (ts > 60) { 1458 | tm = Math.floor(ts / 60); 1459 | ts = (ts % 60); 1460 | } 1461 | 1462 | if (tm > 60) { 1463 | th = Math.floor(tm / 60); 1464 | tm = (tm % 60); 1465 | } 1466 | 1467 | if (th > 24) { 1468 | td = Math.floor(th / 24); 1469 | th = (th % 24); 1470 | } 1471 | 1472 | subst = (td > 0) 1473 | ? String.format('%dd %dh %dm %ds', td, th, tm, ts) 1474 | : String.format('%dh %dm %ds', th, tm, ts); 1475 | 1476 | break; 1477 | 1478 | case 'm': 1479 | var mf = pMinLength ? +pMinLength : 1000; 1480 | var pr = pPrecision ? ~~(10 * +('0' + pPrecision)) : 2; 1481 | 1482 | var i = 0; 1483 | var val = (+param || 0); 1484 | var units = [ ' ', ' K', ' M', ' G', ' T', ' P', ' E' ]; 1485 | 1486 | for (i = 0; (i < units.length) && (val > mf); i++) 1487 | val /= mf; 1488 | 1489 | subst = (i ? val.toFixed(pr) : val) + units[i]; 1490 | pMinLength = null; 1491 | break; 1492 | } 1493 | } 1494 | } 1495 | 1496 | if (pMinLength) { 1497 | subst = subst.toString(); 1498 | for (var i = subst.length; i < pMinLength; i++) 1499 | if (pJustify == '-') 1500 | subst = subst + ' '; 1501 | else 1502 | subst = pad + subst; 1503 | } 1504 | 1505 | out += leftpart + subst; 1506 | str = str.substr(m.length); 1507 | } 1508 | 1509 | return out + str; 1510 | } 1511 | 1512 | String.prototype.nobr = function() 1513 | { 1514 | return this.replace(/[\s\n]+/g, ' '); 1515 | } 1516 | 1517 | String.format = function() 1518 | { 1519 | var a = [ ]; 1520 | for (var i = 1; i < arguments.length; i++) 1521 | a.push(arguments[i]); 1522 | return ''.format.apply(arguments[0], a); 1523 | } 1524 | 1525 | String.nobr = function() 1526 | { 1527 | var a = [ ]; 1528 | for (var i = 1; i < arguments.length; i++) 1529 | a.push(arguments[i]); 1530 | return ''.nobr.apply(arguments[0], a); 1531 | } 1532 | 1533 | if (window.NodeList && !NodeList.prototype.forEach) { 1534 | NodeList.prototype.forEach = function (callback, thisArg) { 1535 | thisArg = thisArg || window; 1536 | for (var i = 0; i < this.length; i++) { 1537 | callback.call(thisArg, this[i], i, this); 1538 | } 1539 | }; 1540 | } 1541 | 1542 | 1543 | var dummyElem, domParser; 1544 | 1545 | function isElem(e) 1546 | { 1547 | return (typeof(e) === 'object' && e !== null && 'nodeType' in e); 1548 | } 1549 | 1550 | function toElem(s) 1551 | { 1552 | var elem; 1553 | 1554 | try { 1555 | domParser = domParser || new DOMParser(); 1556 | elem = domParser.parseFromString(s, 'text/html').body.firstChild; 1557 | } 1558 | catch(e) {} 1559 | 1560 | if (!elem) { 1561 | try { 1562 | dummyElem = dummyElem || document.createElement('div'); 1563 | dummyElem.innerHTML = s; 1564 | elem = dummyElem.firstChild; 1565 | } 1566 | catch (e) {} 1567 | } 1568 | 1569 | return elem || null; 1570 | } 1571 | 1572 | function findParent(node, selector) 1573 | { 1574 | while (node) 1575 | if (node.msMatchesSelector && node.msMatchesSelector(selector)) 1576 | return node; 1577 | else if (node.matches && node.matches(selector)) 1578 | return node; 1579 | else 1580 | node = node.parentNode; 1581 | 1582 | return null; 1583 | } 1584 | 1585 | function E() 1586 | { 1587 | var html = arguments[0], 1588 | attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null, 1589 | data = attr ? arguments[2] : arguments[1], 1590 | elem; 1591 | 1592 | if (isElem(html)) 1593 | elem = html; 1594 | else if (html.charCodeAt(0) === 60) 1595 | elem = toElem(html); 1596 | else 1597 | elem = document.createElement(html); 1598 | 1599 | if (!elem) 1600 | return null; 1601 | 1602 | if (attr) 1603 | for (var key in attr) 1604 | if (attr.hasOwnProperty(key) && attr[key] !== null && attr[key] !== undefined) 1605 | elem.setAttribute(key, attr[key]); 1606 | 1607 | if (typeof(data) === 'function') 1608 | data = data(elem); 1609 | 1610 | if (isElem(data)) { 1611 | elem.appendChild(data); 1612 | } 1613 | else if (Array.isArray(data)) { 1614 | for (var i = 0; i < data.length; i++) 1615 | if (isElem(data[i])) 1616 | elem.appendChild(data[i]); 1617 | else 1618 | elem.appendChild(document.createTextNode('' + data[i])); 1619 | } 1620 | else if (data !== null && data !== undefined) { 1621 | elem.innerHTML = '' + data; 1622 | } 1623 | 1624 | return elem; 1625 | } 1626 | 1627 | if (typeof(window.CustomEvent) !== 'function') { 1628 | function CustomEvent(event, params) { 1629 | params = params || { bubbles: false, cancelable: false, detail: undefined }; 1630 | var evt = document.createEvent('CustomEvent'); 1631 | evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); 1632 | return evt; 1633 | } 1634 | 1635 | CustomEvent.prototype = window.Event.prototype; 1636 | window.CustomEvent = CustomEvent; 1637 | } 1638 | 1639 | CBIDropdown = { 1640 | openDropdown: function(sb) { 1641 | var st = window.getComputedStyle(sb, null), 1642 | ul = sb.querySelector('ul'), 1643 | li = ul.querySelectorAll('li'), 1644 | sel = ul.querySelector('[selected]'), 1645 | rect = sb.getBoundingClientRect(), 1646 | h = sb.clientHeight - parseFloat(st.paddingTop) - parseFloat(st.paddingBottom), 1647 | mh = this.dropdown_items * h, 1648 | eh = Math.min(mh, li.length * h); 1649 | 1650 | document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { 1651 | s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); 1652 | }); 1653 | 1654 | ul.style.maxHeight = mh + 'px'; 1655 | sb.setAttribute('open', ''); 1656 | 1657 | ul.scrollTop = sel ? Math.max(sel.offsetTop - sel.offsetHeight, 0) : 0; 1658 | ul.querySelectorAll('[selected] input[type="checkbox"]').forEach(function(c) { 1659 | c.checked = true; 1660 | }); 1661 | 1662 | ul.style.top = ul.style.bottom = ''; 1663 | ul.style[((sb.getBoundingClientRect().top + eh) > window.innerHeight) ? 'bottom' : 'top'] = rect.height + 'px'; 1664 | ul.classList.add('dropdown'); 1665 | 1666 | var pv = ul.cloneNode(true); 1667 | pv.classList.remove('dropdown'); 1668 | pv.classList.add('preview'); 1669 | 1670 | sb.insertBefore(pv, ul.nextElementSibling); 1671 | 1672 | li.forEach(function(l) { 1673 | l.setAttribute('tabindex', 0); 1674 | }); 1675 | 1676 | sb.lastElementChild.setAttribute('tabindex', 0); 1677 | 1678 | this.setFocus(sb, sel || li[0], true); 1679 | }, 1680 | 1681 | closeDropdown: function(sb, no_focus) { 1682 | if (!sb.hasAttribute('open')) 1683 | return; 1684 | 1685 | var pv = sb.querySelector('ul.preview'), 1686 | ul = sb.querySelector('ul.dropdown'), 1687 | li = ul.querySelectorAll('li'); 1688 | 1689 | li.forEach(function(l) { l.removeAttribute('tabindex'); }); 1690 | sb.lastElementChild.removeAttribute('tabindex'); 1691 | 1692 | sb.removeChild(pv); 1693 | sb.removeAttribute('open'); 1694 | sb.style.width = sb.style.height = ''; 1695 | 1696 | ul.classList.remove('dropdown'); 1697 | 1698 | if (!no_focus) 1699 | this.setFocus(sb, sb); 1700 | 1701 | this.saveValues(sb, ul); 1702 | }, 1703 | 1704 | toggleItem: function(sb, li, force_state) { 1705 | if (li.hasAttribute('unselectable')) 1706 | return; 1707 | 1708 | if (this.multi) { 1709 | var cbox = li.querySelector('input[type="checkbox"]'), 1710 | items = li.parentNode.querySelectorAll('li'), 1711 | label = sb.querySelector('ul.preview'), 1712 | sel = li.parentNode.querySelectorAll('[selected]').length, 1713 | more = sb.querySelector('.more'), 1714 | ndisplay = this.display_items, 1715 | n = 0; 1716 | 1717 | if (li.hasAttribute('selected')) { 1718 | if (force_state !== true) { 1719 | if (sel > 1 || this.optional) { 1720 | li.removeAttribute('selected'); 1721 | cbox.checked = cbox.disabled = false; 1722 | sel--; 1723 | } 1724 | else { 1725 | cbox.disabled = true; 1726 | } 1727 | } 1728 | } 1729 | else { 1730 | if (force_state !== false) { 1731 | li.setAttribute('selected', ''); 1732 | cbox.checked = true; 1733 | cbox.disabled = false; 1734 | sel++; 1735 | } 1736 | } 1737 | 1738 | while (label.firstElementChild) 1739 | label.removeChild(label.firstElementChild); 1740 | 1741 | for (var i = 0; i < items.length; i++) { 1742 | items[i].removeAttribute('display'); 1743 | if (items[i].hasAttribute('selected')) { 1744 | if (ndisplay-- > 0) { 1745 | items[i].setAttribute('display', n++); 1746 | label.appendChild(items[i].cloneNode(true)); 1747 | } 1748 | var c = items[i].querySelector('input[type="checkbox"]'); 1749 | if (c) 1750 | c.disabled = (sel == 1 && !this.optional); 1751 | } 1752 | } 1753 | 1754 | if (ndisplay < 0) 1755 | sb.setAttribute('more', ''); 1756 | else 1757 | sb.removeAttribute('more'); 1758 | 1759 | if (ndisplay === this.display_items) 1760 | sb.setAttribute('empty', ''); 1761 | else 1762 | sb.removeAttribute('empty'); 1763 | 1764 | more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···'; 1765 | } 1766 | else { 1767 | var sel = li.parentNode.querySelector('[selected]'); 1768 | if (sel) { 1769 | sel.removeAttribute('display'); 1770 | sel.removeAttribute('selected'); 1771 | } 1772 | 1773 | li.setAttribute('display', 0); 1774 | li.setAttribute('selected', ''); 1775 | 1776 | this.closeDropdown(sb, true); 1777 | } 1778 | 1779 | this.saveValues(sb, li.parentNode); 1780 | }, 1781 | 1782 | transformItem: function(sb, li) { 1783 | var cbox = E('form', {}, E('input', { type: 'checkbox', tabindex: -1, onclick: 'event.preventDefault()' })), 1784 | label = E('label'); 1785 | 1786 | while (li.firstChild) 1787 | label.appendChild(li.firstChild); 1788 | 1789 | li.appendChild(cbox); 1790 | li.appendChild(label); 1791 | }, 1792 | 1793 | saveValues: function(sb, ul) { 1794 | var sel = ul.querySelectorAll('[selected]'), 1795 | div = sb.lastElementChild; 1796 | 1797 | while (div.lastElementChild) 1798 | div.removeChild(div.lastElementChild); 1799 | 1800 | sel.forEach(function (s) { 1801 | div.appendChild(E('input', { 1802 | type: 'hidden', 1803 | name: s.hasAttribute('name') ? s.getAttribute('name') : (sb.getAttribute('name') || ''), 1804 | value: s.hasAttribute('value') ? s.getAttribute('value') : s.innerText 1805 | })); 1806 | }); 1807 | 1808 | cbi_d_update(); 1809 | }, 1810 | 1811 | setFocus: function(sb, elem, scroll) { 1812 | if (sb && sb.hasAttribute && sb.hasAttribute('locked-in')) 1813 | return; 1814 | 1815 | document.querySelectorAll('.focus').forEach(function(e) { 1816 | if (e.nodeName.toLowerCase() !== 'input') { 1817 | e.classList.remove('focus'); 1818 | e.blur(); 1819 | } 1820 | }); 1821 | 1822 | if (elem) { 1823 | elem.focus(); 1824 | elem.classList.add('focus'); 1825 | 1826 | if (scroll) 1827 | elem.parentNode.scrollTop = elem.offsetTop - elem.parentNode.offsetTop; 1828 | } 1829 | }, 1830 | 1831 | createItems: function(sb, value) { 1832 | var sbox = this, 1833 | val = (value || '').trim().split(/\s+/), 1834 | ul = sb.querySelector('ul'); 1835 | 1836 | if (!sbox.multi) 1837 | val.length = Math.min(val.length, 1); 1838 | 1839 | val.forEach(function(item) { 1840 | var new_item = null; 1841 | 1842 | ul.childNodes.forEach(function(li) { 1843 | if (li.getAttribute && li.getAttribute('value') === item) 1844 | new_item = li; 1845 | }); 1846 | 1847 | if (!new_item) { 1848 | var markup, 1849 | tpl = sb.querySelector(sbox.template); 1850 | 1851 | if (tpl) 1852 | markup = (tpl.textContent || tpl.innerHTML || tpl.firstChild.data).replace(/^$/, '').trim(); 1853 | else 1854 | markup = '
  • {{value}}
  • '; 1855 | 1856 | new_item = E(markup.replace(/{{value}}/g, item)); 1857 | 1858 | if (sbox.multi) { 1859 | sbox.transformItem(sb, new_item); 1860 | } 1861 | else { 1862 | var old = ul.querySelector('li[created]'); 1863 | if (old) 1864 | ul.removeChild(old); 1865 | 1866 | new_item.setAttribute('created', ''); 1867 | } 1868 | 1869 | new_item = ul.insertBefore(new_item, ul.lastElementChild); 1870 | } 1871 | 1872 | sbox.toggleItem(sb, new_item, true); 1873 | sbox.setFocus(sb, new_item, true); 1874 | }); 1875 | }, 1876 | 1877 | closeAllDropdowns: function() { 1878 | document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { 1879 | s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); 1880 | }); 1881 | } 1882 | }; 1883 | 1884 | function cbi_dropdown_init(sb) { 1885 | if (!(this instanceof cbi_dropdown_init)) 1886 | return new cbi_dropdown_init(sb); 1887 | 1888 | this.multi = sb.hasAttribute('multiple'); 1889 | this.optional = sb.hasAttribute('optional'); 1890 | this.placeholder = sb.getAttribute('placeholder') || '---'; 1891 | this.display_items = parseInt(sb.getAttribute('display-items') || 3); 1892 | this.dropdown_items = parseInt(sb.getAttribute('dropdown-items') || 5); 1893 | this.create = sb.getAttribute('item-create') || '.create-item-input'; 1894 | this.template = sb.getAttribute('item-template') || 'script[type="item-template"]'; 1895 | 1896 | var sbox = this, 1897 | ul = sb.querySelector('ul'), 1898 | items = ul.querySelectorAll('li'), 1899 | more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')), 1900 | open = sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')), 1901 | canary = sb.appendChild(E('div')), 1902 | create = sb.querySelector(this.create), 1903 | ndisplay = this.display_items, 1904 | n = 0; 1905 | 1906 | if (this.multi) { 1907 | for (var i = 0; i < items.length; i++) { 1908 | sbox.transformItem(sb, items[i]); 1909 | 1910 | if (items[i].hasAttribute('selected') && ndisplay-- > 0) 1911 | items[i].setAttribute('display', n++); 1912 | } 1913 | } 1914 | else { 1915 | var sel = sb.querySelectorAll('[selected]'); 1916 | 1917 | sel.forEach(function(s) { 1918 | s.removeAttribute('selected'); 1919 | }); 1920 | 1921 | var s = sel[0] || items[0]; 1922 | if (s) { 1923 | s.setAttribute('selected', ''); 1924 | s.setAttribute('display', n++); 1925 | } 1926 | 1927 | ndisplay--; 1928 | 1929 | if (this.optional && !ul.querySelector('li[value=""]')) { 1930 | var placeholder = E('li', { placeholder: '' }, this.placeholder); 1931 | ul.firstChild ? ul.insertBefore(placeholder, ul.firstChild) : ul.appendChild(placeholder); 1932 | } 1933 | } 1934 | 1935 | sbox.saveValues(sb, ul); 1936 | 1937 | ul.setAttribute('tabindex', -1); 1938 | sb.setAttribute('tabindex', 0); 1939 | 1940 | if (ndisplay < 0) 1941 | sb.setAttribute('more', '') 1942 | else 1943 | sb.removeAttribute('more'); 1944 | 1945 | if (ndisplay === this.display_items) 1946 | sb.setAttribute('empty', '') 1947 | else 1948 | sb.removeAttribute('empty'); 1949 | 1950 | more.innerHTML = (ndisplay === this.display_items) ? this.placeholder : '···'; 1951 | 1952 | 1953 | sb.addEventListener('click', function(ev) { 1954 | if (!this.hasAttribute('open')) { 1955 | if (ev.target.nodeName.toLowerCase() !== 'input') 1956 | sbox.openDropdown(this); 1957 | } 1958 | else { 1959 | var li = findParent(ev.target, 'li'); 1960 | if (li && li.parentNode.classList.contains('dropdown')) 1961 | sbox.toggleItem(this, li); 1962 | } 1963 | 1964 | ev.preventDefault(); 1965 | ev.stopPropagation(); 1966 | }); 1967 | 1968 | sb.addEventListener('keydown', function(ev) { 1969 | if (ev.target.nodeName.toLowerCase() === 'input') 1970 | return; 1971 | 1972 | if (!this.hasAttribute('open')) { 1973 | switch (ev.keyCode) { 1974 | case 37: 1975 | case 38: 1976 | case 39: 1977 | case 40: 1978 | sbox.openDropdown(this); 1979 | ev.preventDefault(); 1980 | } 1981 | } 1982 | else 1983 | { 1984 | var active = findParent(document.activeElement, 'li'); 1985 | 1986 | switch (ev.keyCode) { 1987 | case 27: 1988 | sbox.closeDropdown(this); 1989 | break; 1990 | 1991 | case 13: 1992 | if (active) { 1993 | if (!active.hasAttribute('selected')) 1994 | sbox.toggleItem(this, active); 1995 | sbox.closeDropdown(this); 1996 | ev.preventDefault(); 1997 | } 1998 | break; 1999 | 2000 | case 32: 2001 | if (active) { 2002 | sbox.toggleItem(this, active); 2003 | ev.preventDefault(); 2004 | } 2005 | break; 2006 | 2007 | case 38: 2008 | if (active && active.previousElementSibling) { 2009 | sbox.setFocus(this, active.previousElementSibling); 2010 | ev.preventDefault(); 2011 | } 2012 | break; 2013 | 2014 | case 40: 2015 | if (active && active.nextElementSibling) { 2016 | sbox.setFocus(this, active.nextElementSibling); 2017 | ev.preventDefault(); 2018 | } 2019 | break; 2020 | } 2021 | } 2022 | }); 2023 | 2024 | sb.addEventListener('cbi-dropdown-close', function(ev) { 2025 | sbox.closeDropdown(this, true); 2026 | }); 2027 | 2028 | if ('ontouchstart' in window) { 2029 | sb.addEventListener('touchstart', function(ev) { ev.stopPropagation(); }); 2030 | window.addEventListener('touchstart', sbox.closeAllDropdowns); 2031 | } 2032 | else { 2033 | sb.addEventListener('mouseover', function(ev) { 2034 | if (!this.hasAttribute('open')) 2035 | return; 2036 | 2037 | var li = findParent(ev.target, 'li'); 2038 | if (li) { 2039 | if (li.parentNode.classList.contains('dropdown')) 2040 | sbox.setFocus(this, li); 2041 | 2042 | ev.stopPropagation(); 2043 | } 2044 | }); 2045 | 2046 | sb.addEventListener('focus', function(ev) { 2047 | document.querySelectorAll('.cbi-dropdown[open]').forEach(function(s) { 2048 | if (s !== this || this.hasAttribute('open')) 2049 | s.dispatchEvent(new CustomEvent('cbi-dropdown-close', {})); 2050 | }); 2051 | }); 2052 | 2053 | canary.addEventListener('focus', function(ev) { 2054 | sbox.closeDropdown(this.parentNode); 2055 | }); 2056 | 2057 | window.addEventListener('mouseover', sbox.setFocus); 2058 | window.addEventListener('click', sbox.closeAllDropdowns); 2059 | } 2060 | 2061 | if (create) { 2062 | create.addEventListener('keydown', function(ev) { 2063 | switch (ev.keyCode) { 2064 | case 13: 2065 | sbox.createItems(sb, this.value); 2066 | ev.preventDefault(); 2067 | this.value = ''; 2068 | this.blur(); 2069 | break; 2070 | } 2071 | }); 2072 | 2073 | create.addEventListener('focus', function(ev) { 2074 | var cbox = findParent(this, 'li').querySelector('input[type="checkbox"]'); 2075 | if (cbox) cbox.checked = true; 2076 | sb.setAttribute('locked-in', ''); 2077 | }); 2078 | 2079 | create.addEventListener('blur', function(ev) { 2080 | var cbox = findParent(this, 'li').querySelector('input[type="checkbox"]'); 2081 | if (cbox) cbox.checked = false; 2082 | sb.removeAttribute('locked-in'); 2083 | }); 2084 | 2085 | var li = findParent(create, 'li'); 2086 | 2087 | li.setAttribute('unselectable', ''); 2088 | li.addEventListener('click', function(ev) { 2089 | this.querySelector(sbox.create).focus(); 2090 | }); 2091 | } 2092 | } 2093 | 2094 | cbi_dropdown_init.prototype = CBIDropdown; 2095 | 2096 | function cbi_update_table(table, data, placeholder) { 2097 | target = isElem(table) ? table : document.querySelector(table); 2098 | 2099 | if (!isElem(target)) 2100 | return; 2101 | 2102 | target.querySelectorAll('.tr.table-titles, .cbi-section-table-titles').forEach(function(thead) { 2103 | var titles = []; 2104 | 2105 | thead.querySelectorAll('.th').forEach(function(th) { 2106 | titles.push(th); 2107 | }); 2108 | 2109 | if (Array.isArray(data)) { 2110 | var n = 0, rows = target.querySelectorAll('.tr'); 2111 | 2112 | data.forEach(function(row) { 2113 | var trow = E('div', { 'class': 'tr' }); 2114 | 2115 | for (var i = 0; i < titles.length; i++) { 2116 | var text = (titles[i].innerText || '').trim(); 2117 | var td = trow.appendChild(E('div', { 2118 | 'class': titles[i].className, 2119 | 'data-title': (text !== '') ? text : null 2120 | }, row[i] || '')); 2121 | 2122 | td.classList.remove('th'); 2123 | td.classList.add('td'); 2124 | } 2125 | 2126 | trow.classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1)); 2127 | 2128 | if (rows[n]) 2129 | target.replaceChild(trow, rows[n]); 2130 | else 2131 | target.appendChild(trow); 2132 | }); 2133 | 2134 | while (rows[++n]) 2135 | target.removeChild(rows[n]); 2136 | 2137 | if (placeholder && target.firstElementChild === target.lastElementChild) { 2138 | var trow = target.appendChild(E('div', { 'class': 'tr placeholder' })); 2139 | var td = trow.appendChild(E('div', { 'class': titles[0].className }, placeholder)); 2140 | 2141 | td.classList.remove('th'); 2142 | td.classList.add('td'); 2143 | } 2144 | } 2145 | else { 2146 | thead.parentNode.style.display = 'none'; 2147 | 2148 | thead.parentNode.querySelectorAll('.tr, .cbi-section-table-row').forEach(function(trow) { 2149 | if (trow !== thead) { 2150 | var n = 0; 2151 | trow.querySelectorAll('.th, .td').forEach(function(td) { 2152 | if (n < titles.length) { 2153 | var text = (titles[n++].innerText || '').trim(); 2154 | if (text !== '') 2155 | td.setAttribute('data-title', text); 2156 | } 2157 | }); 2158 | } 2159 | }); 2160 | 2161 | thead.parentNode.style.display = ''; 2162 | } 2163 | }); 2164 | } 2165 | 2166 | document.addEventListener('DOMContentLoaded', function() { 2167 | document.querySelectorAll('.table').forEach(cbi_update_table); 2168 | }); 2169 | -------------------------------------------------------------------------------- /src/webSite/static/scripts/xhr.js: -------------------------------------------------------------------------------- 1 | /* 2 | * xhr.js - XMLHttpRequest helper class 3 | * (c) 2008-2010 Jo-Philipp Wich 4 | */ 5 | 6 | XHR = function() 7 | { 8 | this.reinit = function() 9 | { 10 | if (window.XMLHttpRequest) { 11 | this._xmlHttp = new XMLHttpRequest(); 12 | } 13 | else if (window.ActiveXObject) { 14 | this._xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); 15 | } 16 | else { 17 | alert("xhr.js: XMLHttpRequest is not supported by this browser!"); 18 | } 19 | } 20 | 21 | this.busy = function() { 22 | if (!this._xmlHttp) 23 | return false; 24 | 25 | switch (this._xmlHttp.readyState) 26 | { 27 | case 1: 28 | case 2: 29 | case 3: 30 | return true; 31 | 32 | default: 33 | return false; 34 | } 35 | } 36 | 37 | this.abort = function() { 38 | if (this.busy()) 39 | this._xmlHttp.abort(); 40 | } 41 | 42 | this.get = function(url,data,callback,timeout) 43 | { 44 | this.reinit(); 45 | 46 | var ts = Date.now(); 47 | var xhr = this._xmlHttp; 48 | var code = this._encode(data); 49 | 50 | url = location.protocol + '//' + location.host + url; 51 | 52 | if (code) 53 | if (url.substr(url.length-1,1) == '&') 54 | url += code; 55 | else 56 | url += '?' + code; 57 | 58 | xhr.open('GET', url, true); 59 | 60 | if (!isNaN(timeout)) 61 | xhr.timeout = timeout; 62 | 63 | xhr.onreadystatechange = function() 64 | { 65 | if (xhr.readyState == 4) { 66 | var json = null; 67 | if (xhr.getResponseHeader("Content-Type") == "application/json") { 68 | try { json = JSON.parse(xhr.responseText); } 69 | catch(e) { json = null; } 70 | } 71 | 72 | callback(xhr, json, Date.now() - ts); 73 | } 74 | } 75 | 76 | xhr.send(null); 77 | } 78 | 79 | this.post = function(url,data,callback,timeout) 80 | { 81 | this.reinit(); 82 | 83 | var ts = Date.now(); 84 | var xhr = this._xmlHttp; 85 | var code = this._encode(data); 86 | 87 | xhr.onreadystatechange = function() 88 | { 89 | if (xhr.readyState == 4) { 90 | var json = null; 91 | if (xhr.getResponseHeader("Content-Type") == "application/json") { 92 | try { json = JSON.parse(xhr.responseText); } 93 | catch(e) { json = null; } 94 | } 95 | 96 | callback(xhr, json, Date.now() - ts); 97 | } 98 | } 99 | 100 | xhr.open('POST', url, true); 101 | 102 | if (!isNaN(timeout)) 103 | xhr.timeout = timeout; 104 | 105 | xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); 106 | xhr.send(code); 107 | } 108 | 109 | this.cancel = function() 110 | { 111 | this._xmlHttp.onreadystatechange = function(){}; 112 | this._xmlHttp.abort(); 113 | } 114 | 115 | this.send_form = function(form,callback,extra_values) 116 | { 117 | var code = ''; 118 | 119 | for (var i = 0; i < form.elements.length; i++) 120 | { 121 | var e = form.elements[i]; 122 | 123 | if (e.options) 124 | { 125 | code += (code ? '&' : '') + 126 | form.elements[i].name + '=' + encodeURIComponent( 127 | e.options[e.selectedIndex].value 128 | ); 129 | } 130 | else if (e.length) 131 | { 132 | for (var j = 0; j < e.length; j++) 133 | if (e[j].name) { 134 | code += (code ? '&' : '') + 135 | e[j].name + '=' + encodeURIComponent(e[j].value); 136 | } 137 | } 138 | else 139 | { 140 | code += (code ? '&' : '') + 141 | e.name + '=' + encodeURIComponent(e.value); 142 | } 143 | } 144 | 145 | if (typeof extra_values == 'object') 146 | for (var key in extra_values) 147 | code += (code ? '&' : '') + 148 | key + '=' + encodeURIComponent(extra_values[key]); 149 | 150 | return( 151 | (form.method == 'get') 152 | ? this.get(form.getAttribute('action'), code, callback) 153 | : this.post(form.getAttribute('action'), code, callback) 154 | ); 155 | } 156 | 157 | this._encode = function(obj) 158 | { 159 | obj = obj ? obj : { }; 160 | obj['_'] = Math.random(); 161 | 162 | if (typeof obj == 'object') 163 | { 164 | var code = ''; 165 | var self = this; 166 | 167 | for (var k in obj) 168 | code += (code ? '&' : '') + 169 | k + '=' + encodeURIComponent(obj[k]); 170 | 171 | return code; 172 | } 173 | 174 | return obj; 175 | } 176 | } 177 | 178 | XHR.get = function(url, data, callback) 179 | { 180 | (new XHR()).get(url, data, callback); 181 | } 182 | 183 | XHR.poll = function(interval, url, data, callback, post) 184 | { 185 | if (isNaN(interval) || interval < 1) 186 | interval = 5; 187 | 188 | if (!XHR._q) 189 | { 190 | XHR._t = 0; 191 | XHR._q = [ ]; 192 | XHR._r = function() { 193 | for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i]) 194 | { 195 | if (!(XHR._t % e.interval) && !e.xhr.busy()) 196 | e.xhr[post ? 'post' : 'get'](e.url, e.data, e.callback, e.interval * 1000 * 5 - 5); 197 | } 198 | 199 | XHR._t++; 200 | }; 201 | } 202 | 203 | var e = { 204 | interval: interval, 205 | callback: callback, 206 | url: url, 207 | data: data, 208 | xhr: new XHR() 209 | }; 210 | 211 | XHR._q.push(e); 212 | 213 | return e; 214 | } 215 | 216 | XHR.stop = function(e) 217 | { 218 | for (var i = 0; XHR._q && XHR._q[i]; i++) { 219 | if (XHR._q[i] === e) { 220 | e.xhr.cancel(); 221 | XHR._q.splice(i, 1); 222 | return true; 223 | } 224 | } 225 | 226 | return false; 227 | } 228 | 229 | XHR.halt = function() 230 | { 231 | if (XHR._i) 232 | { 233 | /* show & set poll indicator */ 234 | try { 235 | document.getElementById('xhr_poll_status').style.display = ''; 236 | document.getElementById('xhr_poll_status_on').style.display = 'none'; 237 | document.getElementById('xhr_poll_status_off').style.display = ''; 238 | } catch(e) { } 239 | 240 | window.clearInterval(XHR._i); 241 | XHR._i = null; 242 | } 243 | } 244 | 245 | XHR.run = function() 246 | { 247 | if (XHR._r && !XHR._i) 248 | { 249 | /* show & set poll indicator */ 250 | try { 251 | document.getElementById('xhr_poll_status').style.display = ''; 252 | document.getElementById('xhr_poll_status_on').style.display = ''; 253 | document.getElementById('xhr_poll_status_off').style.display = 'none'; 254 | } catch(e) { } 255 | 256 | /* kick first round manually to prevent one second lag when setting up 257 | * the poll interval */ 258 | XHR._r(); 259 | XHR._i = window.setInterval(XHR._r, 1000); 260 | } 261 | } 262 | 263 | XHR.running = function() 264 | { 265 | return !!(XHR._r && XHR._i); 266 | } 267 | 268 | document.addEventListener('DOMContentLoaded', XHR.run); 269 | -------------------------------------------------------------------------------- /src/webSite/templates/configs.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {# 3 | {{header_menus: list of dict, in layout}} 4 | {{form_action: url, in layout}} 5 | {{ sections: dict, as follow }} 6 | 7 | sections:{...:{title, descr, items:[...] }, ... } 8 | 9 | items: 10 | input_type == 'checkbox' 11 | { title, input_type, name, value } 12 | input_type == 'text' 13 | { title, input_type, name, value } 14 | input_type == 'select' 15 | { title, input_type, name, value, options:[{value, display}, ... ] } 16 | #} 17 | {% block head %}设定{% endblock %} 18 | {% block content %}设定{% endblock %} 19 | {% block descr %}设定的更改{% endblock %} 20 | {% block sections %} 21 | {% for section in sections.values() %} 22 |
    23 |

    {{ section['title'] }}

    24 |
    25 | {% for line in section['descr'].split('\n') %} 26 | {{ line }}
    27 | {% endfor %} 28 |
    29 |
    30 | {% for item in section['items'] %} 31 |
    32 | 33 |
    34 | 35 | {% if item['input_type'] == 'checkbox' %} 36 | {% if item['value'] == True %} 37 | 38 | {% else %} 39 | 40 | {% endif %} 41 | 42 | {% elif item['input_type'] == 'text' %} 43 | 44 | 45 | {% elif item['input_type'] == 'select' %} 46 | 55 | 56 | {% endif %} 57 |
    58 |
    59 | {% endfor %} 60 |
    61 |
    62 | {% endfor %} 63 | {% endblock %} 64 | {% block actions %} 65 |
    66 | 69 | 72 |
    73 | {% endblock %} -------------------------------------------------------------------------------- /src/webSite/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {# 4 | {block head} 5 | {{header_menus: dict}} 6 | {{form_action: url}} 7 | {block form 8 | {{map_id: str}} 9 | {block content} 10 | {block descr} 11 | {block sections} 12 | {block actions} 13 | } 14 | #} 15 | 16 | 17 | autoLive {% block head %}WEB{% endblock %} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
    28 |
    29 |
    30 | autoLive 31 | 67 |
    68 | 72 |
    73 |
    74 |
    75 |
    76 | 77 |
    78 | 84 | {% block form %} 85 |
    86 |
    87 |

    {% block content %}H2{% endblock %}

    88 |
    {% block descr %}Here's a scentence.{% endblock %}
    89 | {% block sections %} 90 | {% endblock %} 91 |
    92 | {% block actions %} 93 |
    94 | 95 | 96 | 97 |
    98 | {% endblock %} 99 |
    100 | {% endblock %} 101 | 105 |
    106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/webSite/templates/scheduler.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {# 3 | {{header_menus: list of dict, in layout}} 4 | {{form_action: url, in layout}} 5 | {{ sections: dict, as follow }} 6 | sections:{ 7 | add_job:{ 8 | title, descr, 9 | table_titles:[table_title], 10 | row: [{input_type, name, [contains]}, ...] 11 | }, 12 | schedule_jobs:{ 13 | title, descr, 14 | table_titles:[...] 15 | rows:[{values:[...], id}, ...] 16 | 17 | }, 18 | running_jobs:{ 19 | title, descr, 20 | jobs:[ 21 | { 22 | title, 23 | rows:[{title, value}, ...] 24 | },{...} 25 | ] 26 | } 27 | } 28 | #} 29 | {% block head %}时间表{% endblock %} 30 | {% block content %}时间表{% endblock %} 31 | {% block descr %}时间表的增删改查{% endblock %} 32 | {% block sections %} 33 | 34 | 35 | {# add_job #} 36 |
    37 |

    {{ sections['add_job']['title'] }}

    38 |
    39 | {% for line in sections['add_job']['descr'].split('\n') %} 40 | {{ line }}
    41 | {% endfor %} 42 |
    43 | 44 |
    45 |
    46 | {% for table_title in sections['add_job']['table_titles'] %} 47 |
    48 | {{ table_title }} 49 |
    50 | {% endfor %} 51 |
    52 |
    53 | 54 |
    55 | {% for value in sections['add_job']['row'] %} 56 |
    57 | {% if value['input_type'] == 'text' %} 58 | 59 | {% elif value['input_type'] == 'select' %} 60 | 65 | {% endif %} 66 |
    67 | {% endfor %} 68 |
    69 | 72 |
    73 |
    74 |
    75 |
    76 | 77 | 78 | {# schedule_jobs #} 79 |
    80 |

    {{ sections['schedule_jobs']['title'] }}

    81 |
    82 | {% for line in sections['schedule_jobs']['descr'].split('\n') %} 83 | {{ line }}
    84 | {% endfor %} 85 |
    86 | 87 |
    88 |
    89 | {% for table_title in sections['schedule_jobs']['table_titles'] %} 90 |
    91 | {{ table_title }} 92 |
    93 | {% endfor %} 94 |
    95 |
    96 | 97 | {% for row in sections['schedule_jobs']['rows'] %} 98 |
    99 | {% for value in row['values'] %} 100 |
    101 | {{value}} 102 |
    103 | {% endfor %} 104 |
    105 | 108 |
    109 |
    110 | {% endfor %} 111 |
    112 |
    113 | 114 | 115 | {# running_jobs #} 116 |
    117 |

    {{ sections['running_jobs']['title'] }}

    118 |
    119 | {% for line in sections['running_jobs']['descr'].split('\n') %} 120 | {{ line }}
    121 | {% endfor %} 122 |
    123 | {% for job in sections['running_jobs']['jobs'] %} 124 |
    {{ job['title'] }}
    125 |
    126 | {% for row in job['rows'] %} 127 |
    128 |
    {{ row['title'] }}
    129 |
    {{ row['value'] }}
    130 |
    131 | {% endfor %} 132 |
    133 | {% endfor %} 134 |
    135 | 136 | {% endblock %} 137 | {% block actions %} 138 | {% endblock %} -------------------------------------------------------------------------------- /src/webSite/views/autoLive.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask import render_template, redirect, url_for, abort 3 | from flask import request as flRequest 4 | 5 | from src.webSite.models.autoLive import header_menus, schedule_sections, configs_sections 6 | from src.liveScheduler import LiveScheduler, Live 7 | from src.rebroadcast import rebroadcast 8 | from src.Configs import CONFIGs 9 | 10 | mod = Blueprint('autoLive', __name__, url_prefix='/autoLive') 11 | 12 | 13 | @mod.route('/') 14 | def autoLive_root(): 15 | if len(flRequest.args) != 0: 16 | abort(401) 17 | return redirect(url_for('autoLive.schedule')) 18 | 19 | @mod.route('/schedule', methods=['GET', 'POST']) 20 | def schedule(): 21 | '''处理时间表增删查的网页 22 | 23 | 增加项目时POST存在参数Add 24 | 删除项目时POST存在参数Delete 25 | Delete的值为删除项的live_id 26 | ''' 27 | if flRequest.method == 'POST': 28 | if flRequest.form.get('Add', None): 29 | try: 30 | live = Live( 31 | time=flRequest.form['time'], 32 | liver=flRequest.form['liver'], 33 | site=flRequest.form['site'], 34 | title=flRequest.form['title'] 35 | ) 36 | LiveScheduler().add_live(rebroadcast, live) 37 | except Exception: 38 | pass 39 | elif flRequest.form.get('Delete', None): 40 | LiveScheduler().pop_live(flRequest.form['Delete']) 41 | else: 42 | abort(401) 43 | return redirect(url_for('autoLive.schedule')) 44 | return render_template( 45 | 'scheduler.html', 46 | header_menus=header_menus(), 47 | form_action=url_for('autoLive.schedule'), 48 | sections=schedule_sections() 49 | ) 50 | 51 | @mod.route('/configs', methods=['GET', 'POST']) 52 | def configs(): 53 | '''设置页面 54 | 55 | POST访问时动作由参数Action值决定 56 | 为Apply时应用form中设置 57 | 为Reset时丢弃form中设置 58 | ''' 59 | if flRequest.method == 'POST' and flRequest.form['Action'] == 'Apply': 60 | config = CONFIGs() 61 | 62 | config.BILIBILI_ROOM_TITLE = flRequest.form['BILIBILI_ROOM_TITLE'] 63 | config.DEFAULT_TITLE_PARAM = flRequest.form['DEFAULT_TITLE_PARAM'] 64 | config.BILIBILI_ROOM_AREA_ID = int(flRequest.form['BILIBILI_ROOM_AREA_ID']) 65 | config.IS_SEND_DAILY_DYNAMIC = True if flRequest.form.get('IS_SEND_DAILY_DYNAMIC') else False 66 | config.DAILY_DYNAMIC_FORM = flRequest.form['DAILY_DYNAMIC_FORM'] 67 | config.IS_SEND_PRELIVE_DYNAMIC = True if flRequest.form.get('IS_SEND_PRELIVE_DYNAMIC') else False 68 | config.PRELIVE_DYNAMIC_FORM = flRequest.form['PRELIVE_DYNAMIC_FORM'] 69 | 70 | config.LIVE_QUALITY = int(flRequest.form['LIVE_QUALITY']) 71 | config.save_configs() 72 | return redirect(url_for('autoLive.schedule')) 73 | return render_template( 74 | 'configs.html', 75 | header_menus=header_menus(), 76 | form_action=url_for('autoLive.configs'), 77 | sections=configs_sections() 78 | ) -------------------------------------------------------------------------------- /updateConfigs.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from configparser import ConfigParser 3 | 4 | 5 | def SetupConfigs(): 6 | COOKIE_FAILED = False 7 | PORT_FAILED = False 8 | try: 9 | print('###############################') 10 | print('# #') 11 | print('# paste your bilibili cookies #') 12 | print('# #') 13 | print('###############################') 14 | print('粘贴B站cookies。注意不要把开头的“Cookie:”也一起复制。') 15 | print('如果此处写入失败,请手动把Cookie写入cookies.txt') 16 | cookies = input('粘贴cookies到此处:') 17 | with open('cookies.txt', 'w', encoding='utf-8') as f: 18 | f.write(cookies) 19 | except: 20 | print('cookies写入失败,请手动写入cookies.txt') 21 | COOKIE_FAILED = True 22 | try: 23 | new_configs = ConfigParser() 24 | new_configs.read('config.ini', encoding='utf-8') 25 | print('请输入打算使用的端口号,必须是0~65535之间的一个数字,并避开80、22等常用端口。') 26 | print('如果此处设置不成功,也可在config.ini设置') 27 | port = input('请输入端口号(推荐输入2434):') 28 | port = int(port) 29 | if port < 0 or 65535 < port: 30 | raise Exception 31 | new_configs.set('basic', 'WEB_PORT', str(port)) 32 | new_configs.write(open('config.ini', 'w', encoding='utf-8')) 33 | except: 34 | print('端口号设置失败,请手动从config.ini设置。') 35 | PORT_FAILED = True 36 | 37 | print('###############################') 38 | print('从ver2.1.0起一键安装脚本不再自动启动程序。') 39 | print('请手动按以下步骤启动。') 40 | print('1. 输入命令:') 41 | print('screen -S autoLive # 打开新的screen窗口并命名为autoLive。在screen中启动的程序即使关闭shell也能继续运行。') 42 | print('2. 输入命令:') 43 | print('cd ~/autoLive # 进入autoLive文件夹。本程序必须从autoLive文件夹中打开。') 44 | print('3. 输入命令:') 45 | print('python36 main.py # 使用python3.6运行本程序。') 46 | print('4. 程序运行后,按ctrl+A后,按下d键退出screen窗口。') 47 | print('此时在screen中启动的程序仍会在后台运行。') 48 | print('可以从浏览器访问http://<服务器ip>:<端口>/autoLive,如果出现界面正常则程序运行成功。') 49 | print('######') 50 | print('停止程序时请按以下步骤停止。') 51 | print('1. 输入命令:') 52 | print('screen -r autoLive # 恢复名为autoLive的screen窗口') 53 | print('2. 按下ctrl+C停止程序,可能需要多按几次。') 54 | print('3. 输入命令:') 55 | print('exit # 退出并关闭screen窗口。') 56 | print('#############################') 57 | print('安装终了!') 58 | if COOKIE_FAILED: 59 | print('cookies写入失败,请手动写入cookies.txt') 60 | if PORT_FAILED: 61 | print('端口号设置失败,请手动从config.ini设置。') 62 | 63 | if __name__ == '__main__': 64 | SetupConfigs() --------------------------------------------------------------------------------