156 lines
6.5 KiB
Python
156 lines
6.5 KiB
Python
import logging
|
|
from datetime import date, datetime, timedelta
|
|
from functools import lru_cache
|
|
from typing import List
|
|
from xml.sax.saxutils import unescape
|
|
|
|
from epg2xml.providers import EPGProgram, EPGProvider
|
|
|
|
log = logging.getLogger(__name__.rsplit(".", maxsplit=1)[-1].upper())
|
|
today = date.today()
|
|
|
|
|
|
class WAVVE(EPGProvider):
|
|
"""EPGProvider for WAVVE
|
|
|
|
데이터: jsonapi
|
|
요청수: 1
|
|
특이사항:
|
|
- 해외나 VPS는 차단 가능성이 높음
|
|
"""
|
|
|
|
referer = "https://www.wavve.com/"
|
|
title_regex = r"^(.*?)(?:\s*[\(<]?([\d]+)회[\)>]?)?(?:\([월화수목금토일]?\))?(\([선별전주\(\)재방]*?재[\d방]?\))?\s*(?:\[(.+)\])?$"
|
|
|
|
base_url = "https://apis.wavve.com"
|
|
base_params = {
|
|
"apikey": "E5F3E0D30947AA5440556471321BB6D9",
|
|
"client_version": "6.0.1",
|
|
"device": "pc",
|
|
"drm": "wm",
|
|
"partner": "pooq",
|
|
"pooqzone": "none",
|
|
"region": "kor",
|
|
"targetage": "all",
|
|
}
|
|
|
|
def __init__(self, cfg):
|
|
super().__init__(cfg)
|
|
self.sess.headers.update({"wavve-credential": "none"})
|
|
|
|
def __url(self, url: str) -> str:
|
|
"""completes partial urls from api response or for api request"""
|
|
if url.startswith(("http://", "https://")):
|
|
return url
|
|
if url.startswith("/"):
|
|
return self.base_url + url
|
|
return "https://" + url
|
|
|
|
def __params(self, **params) -> dict:
|
|
"""returns url parameters for api requests with base ones"""
|
|
p = self.base_params.copy()
|
|
p.update(params)
|
|
return p
|
|
|
|
def __get(self, url: str, **kwargs):
|
|
url = self.__url(url)
|
|
params = self.__params(**kwargs.pop("params", {}))
|
|
return self.request(url, params=params, **kwargs)
|
|
|
|
def get_svc_channels(self) -> List[dict]:
|
|
today_str = today.strftime("%Y-%m-%d")
|
|
hour_min = datetime.now().hour // 3
|
|
# 현재 시간과 가까운 미래에 서비스 가능한 채널만 가져옴
|
|
params = {
|
|
"enddatetime": f"{today_str} {(hour_min+1)*3:02d}:00",
|
|
"genre": "all",
|
|
"limit": 500,
|
|
"offset": 0,
|
|
"startdatetime": f"{today_str} {hour_min*3:02d}:00",
|
|
}
|
|
return [
|
|
{
|
|
"Name": x["channelname"],
|
|
"Icon_url": self.__url(x["channelimage"]),
|
|
"ServiceId": x["channelid"],
|
|
}
|
|
for x in self.__get("/live/epgs", params=params)["list"]
|
|
]
|
|
|
|
def __epg_of_program(self, channelid: str, data: dict) -> EPGProgram:
|
|
_epg = EPGProgram(channelid)
|
|
_epg.stime = datetime.strptime(data["starttime"], "%Y-%m-%d %H:%M")
|
|
_epg.etime = datetime.strptime(data["endtime"], "%Y-%m-%d %H:%M")
|
|
# 채널이름은 그대로 들어오고 프로그램 제목은 escape되어 들어옴
|
|
_epg.title = unescape(data["title"])
|
|
if m := self.title_regex.match(_epg.title):
|
|
_epg.title = m.group(1)
|
|
_epg.title_sub = m.group(4)
|
|
episode = (m.group(2) or "").replace("회", "").strip()
|
|
_epg.ep_num = None if episode == "0" else episode
|
|
_epg.rebroadcast = bool(m.group(3))
|
|
_epg.rating = 0 if data["targetage"] == "n" else int(data["targetage"])
|
|
|
|
# 추가 정보 가져오기
|
|
if not self.cfg["GET_MORE_DETAILS"]:
|
|
return _epg
|
|
programid = data["programid"].strip()
|
|
if not programid:
|
|
# 개별 programid가 없는 경우도 있으니 체크해야함
|
|
return _epg
|
|
detail = self.get_program_details(programid)
|
|
if not detail:
|
|
return _epg
|
|
# 여러가지 추가 정보가 제공되지만
|
|
# 방송되지 않은 미래의 프로그램/에피소드 정보는 반영되지 않았기에
|
|
# 일부 정보만 유효함을 유념
|
|
synopsis = detail["seasonsynopsis"] or detail["programsynopsis"] or detail["episodesynopsis"]
|
|
_epg.desc = "\n".join(
|
|
[x.replace("<br>", "\n").strip() for x in synopsis.splitlines()]
|
|
) # carriage return(\r) 제거, <br> 제거
|
|
_epg.categories = [detail["genretext"].strip()]
|
|
_epg.poster_url = self.__url(detail["seasonposterimage"].strip())
|
|
_epg.keywords = [x["text"] for x in detail["tags"]["list"]]
|
|
actors = detail.get("season_actors") or detail.get("actors") or {"list": []}
|
|
directors = detail.get("season_directors") or detail.get("directors") or {"list": []}
|
|
writers = detail.get("season_writers") or detail.get("writers") or {"list": []}
|
|
_epg.cast = [{"name": x["text"], "title": "actor"} for x in actors["list"]]
|
|
_epg.crew = [{"name": x["text"], "title": "director"} for x in directors["list"]]
|
|
_epg.crew += [{"name": x["text"], "title": "writer"} for x in writers["list"]]
|
|
return _epg
|
|
|
|
def get_programs(self) -> None:
|
|
# parameters for requests
|
|
params = {
|
|
"enddatetime": (today + timedelta(days=int(self.cfg["FETCH_LIMIT"]) - 1)).strftime("%Y-%m-%d 24:00"),
|
|
"genre": "all",
|
|
"limit": 500,
|
|
"offset": 0,
|
|
"startdatetime": today.strftime("%Y-%m-%d 00:00"),
|
|
}
|
|
channeldict = {x["channelid"]: x for x in self.__get("/live/epgs", params=params)["list"]}
|
|
|
|
for idx, _ch in enumerate(self.req_channels):
|
|
log.info("%03d/%03d %s", idx + 1, len(self.req_channels), _ch)
|
|
for program in channeldict[_ch.svcid]["list"]:
|
|
try:
|
|
_epg = self.__epg_of_program(_ch.id, program)
|
|
except Exception:
|
|
log.exception("프로그램 파싱 중 예외: %s", _ch)
|
|
else:
|
|
_ch.programs.append(_epg)
|
|
|
|
@lru_cache
|
|
def get_program_details(self, programid: str) -> dict:
|
|
try:
|
|
params = {"history": "season", "programid": programid}
|
|
data = self.__get("/fz/vod/programs/landing", params=params)
|
|
if data.get("resultcode") in ["550"]:
|
|
# 애초에 유효하지 않은 programid가 있을 수 있음
|
|
# { "resultcode": "550", "resultmessage": "해당 데이터가 없습니다." }
|
|
return None
|
|
return self.__get(f"/fz/vod/contents-detail/{data['content_id'].strip()}")
|
|
except Exception:
|
|
log.exception("프로그램 상세 정보 요청 중 예외: %s", programid)
|
|
return None
|