Skip to content

Commit

Permalink
fflogs update (#34)
Browse files Browse the repository at this point in the history
* 更新 CoolQBot-env 到 0.2.4
* 初步支持从 FFlogs API 计算输出百分比
* 更新伊甸的数据,并支持 adps
* 添加每天的缓存,并支持 pdps
* 定时提前缓存数据
* 添加异常处理
  • Loading branch information
he0119 authored Nov 27, 2019
1 parent 356fcb4 commit 960ada7
Show file tree
Hide file tree
Showing 7 changed files with 207 additions and 69 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

## [Unreleased]

## [0.10.0] - 2019-11-28

### Added

- 添加了查询 fflogs 输出百分比的功能
Expand Down Expand Up @@ -234,7 +236,8 @@

- 正常工作的版本。

[Unreleased]: https://github.com/he0119/CoolQBot/compare/v0.9.1...HEAD
[Unreleased]: https://github.com/he0119/CoolQBot/compare/v0.10.0...HEAD
[0.10.0]: https://github.com/he0119/CoolQBot/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/he0119/CoolQBot/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/he0119/CoolQBot/compare/v0.8.1...v0.9.0
[0.8.1]: https://github.com/he0119/CoolQBot/compare/v0.8.0...v0.8.1
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM he0119/coolqbot-env:v0.2.3
FROM he0119/coolqbot-env:v0.2.4

# 安装依赖
COPY requirements.txt /home/user/coolqbot/requirements.txt
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
nonebot==1.3.1
requests==2.22.0
apscheduler==3.6.1
apscheduler==3.6.3
jieba==0.39
python-dateutil==2.8.0
python-dateutil==2.8.1
msgpack==0.6.2
43 changes: 33 additions & 10 deletions src/plugins/fflogs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,58 @@
从网站 https://cn.fflogs.com/ 获取输出数据
文档网址 https://cn.fflogs.com/v1/docs
"""
import asyncio

from nonebot import CommandSession, on_command

from coolqbot import bot

from .api import API
from .data import boss_list, job_list

HOUR = int(API.data.config_get('cache', 'hour', fallback='4'))
MINUTE = int(API.data.config_get('cache', 'minute', fallback='30'))
SECOND = int(API.data.config_get('cache', 'second', fallback='0'))


@bot.scheduler.scheduled_job(
'cron', hour=HOUR, minute=MINUTE, second=SECOND, id='fflogs_cache'
)
async def fflogs_cache():
""" 定时缓存数据
"""
for (boss_id, difficulty), boss_nickname in boss_list.items():
for job_id, job_nickname in job_list.items():
await API.dps(boss_nickname[0], job_nickname[0])
bot.logger.debug(f'{boss_nickname[0]} {job_nickname[0]}的数据缓存完成。')
await asyncio.sleep(30)


@on_command('dps', aliases=['输出'], only_to_me=False, shell_like=True)
async def dps(session: CommandSession):
""" 查询 DPS
"""
# 设置 Token
if len(session.argv) == 2 and session.argv[0] == 'token':
API.set_token(session.argv[1])
if session.argv[0] == 'token' and len(session.argv) == 2:
API.token = session.argv[1]
session.finish('Token 设置完成。')

# 检查 Token 是否设置
# 现在不需要用到 token 所以注释掉这个检查
# if not API.token:
# session.finish(
# '对不起,Token 未设置,无法查询数据。\n请先使用命令\n/dps token <token>\n配置好 Token 后再尝试查询数据。'
# )
if not API.token:
session.finish(
'对不起,Token 未设置,无法查询数据。\n请先使用命令\n/dps token <token>\n配置好 Token 后再尝试查询数据。'
)

if session.argv[0] == 'token' and len(session.argv) == 1:
session.finish(f'当前的 Token 为 {API.token}')

# if session.argv[0] == 'zones':
# reply = await API.zones()
# session.finish(str(reply[int(session.argv[1])]))
if session.argv[0] == 'classes' and len(session.argv) == 1:
reply = await API.classes()
session.finish(str(reply))

if session.argv[0] == 'zones' and len(session.argv) == 2:
reply = await API.zones()
session.finish(str(reply[int(session.argv[1])]))

if len(session.argv) > 1 and len(session.argv) < 4:
reply = await API.dps(*session.argv)
Expand Down
146 changes: 118 additions & 28 deletions src/plugins/fflogs/api.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,45 @@
""" API
文档网址 https://cn.fflogs.com/v1/docs
"""
import json
import re
import math
from datetime import datetime, timedelta

import aiohttp

from coolqbot import PluginData

from .data import get_boss_info, get_job_name
from .exceptions import AuthException, DataException


class FFlogs:
def __init__(self):
self.base_url = 'https://cn.fflogs.com/v1'
self.data = PluginData('fflogs', config=True)

self.token = self.get_token()
# 当前是 5.0 版本
self.version = self.data.config_get('fflogs', 'version', '0')
# 默认为两周的数据
self.range = self.data.config_get('fflogs', 'range', '14')
# 默认从两周的数据中计算排名百分比
self.range = int(self.data.config_get('fflogs', 'range', '14'))

def set_token(self, token):
self.token = token
self.data.config_set('fflogs', 'token', self.token)

def get_token(self):
@property
def token(self):
try:
return self.data.config_get('fflogs', 'token')
except:
return None

async def _http(self, url, is_json=True):
@token.setter
def token(self, token):
self.data.config_set('fflogs', 'token', token)

async def _http(self, url, is_json=True, headers=None):
try:
# 使用 aiohttp 库发送最终的请求
async with aiohttp.ClientSession() as sess:
async with sess.get(url) as response:
async with sess.get(url, headers=headers) as response:
if response.status == 401:
raise AuthException('Token 有误,无法获取数据')
if response.status != 200:
# 如果 HTTP 响应状态码不是 200,说明调用失败
return None
Expand All @@ -48,38 +53,123 @@ async def _http(self, url, is_json=True):
# 抛出上面任何异常,说明调用失败
return None

async def _get_one_day_ranking(
self, boss, difficulty, job, date: datetime
):
""" 获取指定 boss,指定职业,指定一天中的排名数据
"""
# 查看是否有缓存
cache_name = f'{boss}_{difficulty}_{job}_{date.strftime("%Y%m%d")}'
if self.data.exists(f'{cache_name}.pkl'):
return self.data.load_pkl(cache_name)

page = 1
hasMorePages = True
rankings = []

end_date = date + timedelta(days=1)
# 转换成 API 支持的时间戳格式
start_timestamp = int(date.timestamp()) * 1000
end_timestamp = int(end_date.timestamp()) * 1000

while hasMorePages:
rankings_url = f'{self.base_url}/rankings/encounter/{boss}?metric=rdps&difficulty={difficulty}&spec={job}&page={page}&filter=date.{start_timestamp}.{end_timestamp}&api_key={self.token}'

res = await self._http(rankings_url)

if not res:
raise DataException('服务器没有正确返回数据')

hasMorePages = res['hasMorePages']
rankings += res['rankings']
page += 1

# 如果获取数据的日期不是当天,则缓存数据
# 因为今天的数据可能还会增加,不能先缓存
if end_date < datetime.now():
self.data.save_pkl(rankings, cache_name)

return rankings

async def _get_whole_ranking(
self, boss, difficulty, job, dps_type: str, date: datetime
):
date = datetime(year=date.year, month=date.month, day=date.day)

rankings = []
for _ in range(self.range):
rankings += await self._get_one_day_ranking(
boss, difficulty, job, date
)
date -= timedelta(days=1)

# 根据 DPS 类型进行排序,并提取数据
if dps_type == 'rdps':
rankings.sort(key=lambda x: x['total'], reverse=True)
rankings = [i['total'] for i in rankings]

if dps_type == 'adps':
rankings.sort(
key=lambda x: x['other_per_second_amount'], reverse=True
)
rankings = [i['other_per_second_amount'] for i in rankings]

if dps_type == 'pdps':
rankings.sort(key=lambda x: x['raw_dps'], reverse=True)
rankings = [i['raw_dps'] for i in rankings]

return rankings

async def zones(self):
""" 副本
"""
url = f'{self.base_url}/zones?api_key={self.token}'
data = await self._http(url)
return data

async def classes(self):
""" 职业
"""
url = f'{self.base_url}/classes?api_key={self.token}'
data = await self._http(url)
return data

async def dps(self, boss, job, dps_type='rdps'):
""" 查询 DPS 百分比排名
"""
(boss_bucket, boss_id), boss_name = get_boss_info(boss)
if not boss_bucket:
boss_id, difficulty, boss_name = get_boss_info(boss)
if not boss_id:
return f'找不到 {boss} 的数据,请换个名字试试'

job_name_en, job_name_cn = get_job_name(job)
if not job_name_en:
job_id, job_name = get_job_name(job)
if not job_id:
return f'找不到 {job} 的数据,请换个名字试试'

fflogs_url = f'https://cn.fflogs.com/zone/statistics/table/{boss_bucket}/dps/{boss_id}/100/8/5/100/1/{self.range}/{self.version}/Global/{job_name_en}/All/0/normalized/single/0/-1/?keystone=15&dpstype={dps_type}'

res = await self._http(fflogs_url, is_json=False)
if dps_type not in ['adps', 'rdps', 'pdps']:
return f'找不到类型为 {dps_type} 的数据,只支持 adps rdps pdps'

# 排名从前一天开始排,因为今天的数据并不全
date = datetime.now() - timedelta(days=1)
try:
rankings = await self._get_whole_ranking(
boss_id, difficulty, job_id, dps_type, date
)
except DataException as e:
return f'{e},请稍后再试'
except AuthException as e:
return f'{e},请检查 Token'

reply = f'{boss_name} {job_name} 的数据({dps_type})'

total = len(rankings)
reply += f'\n数据总数:{total} 条'
# 计算百分比的 DPS
percentage_list = [100, 99, 95, 75, 50, 25, 10]
reply = f'{boss_name} {job_name_cn} 的数据({dps_type})'
for perc in percentage_list:
if perc == 100:
re_str = ('series' + r'.data.push\((\d*\.?\d*)\)')
else:
re_str = (f'series{perc}' + r'.data.push\((\d*\.?\d*)\)')
ptn = re.compile(re_str)
find_res = float(ptn.findall(res)[0])
reply += f'\n{perc}% : {find_res:.2f}'
number = math.floor(total * 0.01 * (100 - perc))
dps = float(rankings[number])
reply += f'\n{perc}% : {dps:.2f}'

return reply


Expand Down
62 changes: 35 additions & 27 deletions src/plugins/fflogs/data.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,53 @@
""" 一些数据
"""
boss_list = {
(28, 1045): ['缇坦妮雅', '妖精', '极妖精', '妖灵王', '妖精王', '老婆', '10王'],
(28, 1046): ['无瑕灵君', '肥宅', '极肥宅', '全能王'],
(28, 1049): ['哈迪斯', '老公']
(1045, 0): ['提坦妮雅歼殛战', '缇坦妮雅', '妖精', '极妖精', '妖灵王', '妖精王', '老婆', '10王'],
(1046, 0): ['无瑕灵君', '肥宅', '极肥宅', '全能王'],
(1049, 0): ['哈迪斯', '老公'],
(65, 100): ['至尊伊甸', 'E1', 'e1'],
(66, 100): ['虚空行者','E2', 'e2'],
(67, 100): ['利维亚桑','E3', 'e3'],
(68, 100): ['泰坦','E4', 'e4'],
(65, 0): ['至尊伊甸','E1S', 'e1s', 'E1s', 'e1S'],
(66, 0): ['虚空行者','E2S', 'e2s', 'E2s', 'e2S'],
(67, 0): ['利维亚桑','E3S', 'e3s', 'E3s', 'e3S'],
(68, 0): ['泰坦','E4S', 'e4s', 'E4s', 'e4S'],
} # yapf: disable

job_list = {
'Astrologian': ['占星术士', '占星'],
'Bard': ['吟游诗人', '诗人'],
'BlackMage': ['黑魔法师', '黑魔', '伏地魔', '永动机'],
'Dancer': ['舞者', '舞娘'],
'DarkKnight': ['暗黑骑士', '黑骑', '暗骑', 'DK'],
'Dragoon': ['龙骑士', '龙骑', '躺尸龙', '擦炮工'],
'Gunbreaker': ['绝枪战士', '绝枪', '枪刃', '枪决战士'],
'Machinist': ['机工士', '机工'],
'Monk': ['武僧', '扫地僧', '猴子', '和尚'],
'Ninja': ['忍者', '兔忍', '火影'],
'Paladin': ['骑士', '圣骑', '奶骑'],
'RedMage': ['赤魔法师', '赤魔', '吃馍', '红色治疗'],
'Samurai': ['武士', ''],
'Scholar': ['学者', '小仙女', '死炎法师'],
'Summoner': ['召唤师', '召唤'],
'Warrior': ['战士', '战爹'],
'WhiteMage': ['白魔法师', '白魔', '白膜', '投石机'],
1: ['占星术士', '占星'],
2: ['吟游诗人', '诗人'],
3: ['黑魔法师', '黑魔', '伏地魔', '永动机'],
4: ['暗黑骑士', '黑骑', '暗骑', 'DK'],
5: ['龙骑士', '龙骑', '躺尸龙', '擦炮工'],
6: ['机工士', '机工'],
7: ['武僧', '扫地僧', '猴子', '和尚'],
8: ['忍者', '兔忍', '火影'],
9: ['骑士', '圣骑', '奶骑'],
10: ['学者', '小仙女', '死炎法师'],
11: ['召唤师', '召唤'],
12: ['战士', '战爹'],
13: ['白魔法师', '白魔', '白膜', '投石机'],
14: ['赤魔法师', '赤魔', '吃馍', '红色治疗'],
15: ['武士', ''],
16: ['舞者', '舞娘'],
17: ['绝枪战士', '绝枪', '枪刃', '枪决战士'],
} # yapf: disable


def get_boss_info(name):
""" 根据昵称获取 boss 的 bucket 和 id
""" 根据昵称获取 boss 的 ID 同时返回正式名称
"""
for boss, nickname in boss_list.items():
for (boss_id, difficulty), nickname in boss_list.items():
if name in nickname:
return boss, nickname[0]
return (None, None), None
return boss_id, difficulty, nickname[0]
return None, None, None


def get_job_name(name):
""" 将中文称呼转换成英文称呼
""" 将中文昵称转换成具体的 ID 同时返回正式名称
"""
for name_en, nickname in job_list.items():
for job_id, nickname in job_list.items():
if name in nickname:
return name_en, nickname[0]
return job_id, nickname[0]
return None, None
14 changes: 14 additions & 0 deletions src/plugins/fflogs/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
""" 异常
"""


class DataException(Exception):
""" 数据异常
"""
pass


class AuthException(Exception):
""" 认证异常
"""
pass

0 comments on commit 960ada7

Please sign in to comment.