Skip to content

Commit

Permalink
Trakt component with config flow support (#33)
Browse files Browse the repository at this point in the history
* Update Trakt integration to use config flow

* update when options change

* add hacs.json
  • Loading branch information
engrbm87 authored Apr 21, 2020
1 parent 5096214 commit 5d3dbd4
Show file tree
Hide file tree
Showing 8 changed files with 457 additions and 209 deletions.
24 changes: 24 additions & 0 deletions custom_components/trakt/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"config": {
"step": {
"pick_implementation": { "title": "Pick Authentication Method" }
},
"abort": {
"already_setup": "You can only configure one Trakt instance.",
"authorize_url_timeout": "Timeout generating authorize url.",
"missing_configuration": "The Trakt integration is not configured. Please follow the documentation."
},
"create_entry": { "default": "Successfully authenticated with Trakt." }
},
"options": {
"step": {
"init": {
"data": {
"scan_interval": "Update frequency (minutes)",
"days": "Days to look forward for movies/shows",
"exclude": "list of excluded shows (comma separated)"
}
}
}
}
}
279 changes: 278 additions & 1 deletion custom_components/trakt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,281 @@
https://github.com/custom-components/sensor.trakt
https://github.com/custom-cards/upcoming-media-card
"""
"""
import asyncio
import json
import logging
from datetime import timedelta

import async_timeout
import homeassistant.util.dt as dt_util
import trakt
import voluptuous as vol
from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_CLIENT_ID,
CONF_CLIENT_SECRET,
CONF_EXCLUDE,
CONF_SCAN_INTERVAL,
)
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_track_time_interval
from trakt.calendar import MyShowCalendar
from trakt.tv import TVShow

from . import config_flow
from .const import (
CARD_DEFAULT,
CONF_DAYS,
DATA_TRAKT_CRED,
DATA_UPDATED,
DEFAULT_DAYS,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
)

_LOGGER = logging.getLogger(__name__)

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_CLIENT_ID): cv.string,
vol.Required(CONF_CLIENT_SECRET): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)


async def async_setup(hass, config) -> bool:
"""Set up Trakt integration."""
hass.data[DOMAIN] = {}

if DOMAIN not in config:
return True

config_flow.TraktOAuth2FlowHandler.async_register_implementation(
hass,
config_entry_oauth2_flow.LocalOAuth2Implementation(
hass,
DOMAIN,
config[DOMAIN][CONF_CLIENT_ID],
config[DOMAIN][CONF_CLIENT_SECRET],
OAUTH2_AUTHORIZE,
OAUTH2_TOKEN,
),
)
hass.data.setdefault(
DATA_TRAKT_CRED,
{
CONF_CLIENT_ID: config[DOMAIN][CONF_CLIENT_ID],
CONF_CLIENT_SECRET: config[DOMAIN][CONF_CLIENT_SECRET],
},
)

return True


async def async_setup_entry(hass, entry) -> bool:
"""Set up Trakt from config entry."""

implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)

session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
await session.async_ensure_token_valid()
trakt_data = Trakt_Data(hass, entry, session)

if not await trakt_data.async_setup():
return False
hass.data[DOMAIN] = trakt_data

hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, "sensor")
)
return True


async def async_unload_entry(hass, entry):
"""Unload Trakt integration."""
if hass.data[DOMAIN].unsub_timer:
hass.data[DOMAIN].unsub_timer()

await hass.config_entries.async_forward_entry_unload(entry, "sensor")

hass.data.pop(DOMAIN)

return True


class Trakt_Data:
"""Represent Trakt data."""

def __init__(self, hass, config_entry, implementation):
"""Initialize trakt data."""
self.hass = hass
self.config_entry = config_entry
self.session = config_entry_oauth2_flow.OAuth2Session(
hass, config_entry, implementation
)
self.unsub_timer = None
self.details = {}
self.calendar = None

@property
def days(self):
"""Return number of days to look forward for movies/shows."""
return self.config_entry.options[CONF_DAYS]

@property
def exclude(self):
"""Return list of show titles to exclude."""
return self.config_entry.options[CONF_EXCLUDE] or []

async def async_update(self, *_):
"""Update Trakt data."""
card_json = [CARD_DEFAULT]

try:
self.calendar = await self.hass.async_add_executor_job(
MyShowCalendar, {CONF_DAYS: self.days}
)
except trakt.errors.OAuthException:
_LOGGER.error(
"Trakt api is unauthrized. Please remove the entry and reconfigure."
)
return

if not self.calendar:
_LOGGER.warning("Trakt upcoming calendar is empty")
return

for show in self.calendar:
if not show or show.show in self.exclude:
continue

try:
show_details = await self.hass.async_add_executor_job(
TVShow.search, show.show, show.year
)
except AttributeError:
_LOGGER.error("Unable to retrieve show details for " + show.show)

if not show_details:
continue

if days_until(show.airs_at) < 0:
continue
if days_until(show.airs_at) <= 7:
release = "$day, $time"
else:
release = "$day, $date $time"

session = aiohttp_client.async_get_clientsession(self.hass)
try:
with async_timeout.timeout(10):
response = await session.get(
f"http://api.tmdb.org/3/tv/{str(show_details[0].tmdb)}?api_key=0eee347e2333d7a97b724106353ca42f",
)
except asyncio.TimeoutError:
_LOGGER.warning("api.themoviedb.org is not responding")
continue

if response.status != 200:
_LOGGER.warning("Error retrving information from api.themoviedb.org")
continue

tmdb_json = await response.json()

image_url = "https://image.tmdb.org/t/p/w%s%s"

card_item = {
"airdate": show.airs_at.isoformat() + "Z",
"release": release,
"flag": False,
"title": show.show,
"episode": show.title,
"number": "S" + str(show.season).zfill(2) + "E" + str(show.number),
"rating": tmdb_json.get("vote_average", ""),
"poster": image_url % ("500", tmdb_json.get("poster_path", "")),
"fanart": image_url % ("780", tmdb_json.get("backdrop_path", "")),
"runtime": tmdb_json.get("episode_run_time")[0]
if len(tmdb_json.get("episode_run_time", [])) > 0
else "",
"studio": tmdb_json.get("networks")[0].get("name", "")
if len(tmdb_json.get("networks", [])) > 0
else "",
}
card_json.append(card_item)

self.details = json.dumps(card_json)
_LOGGER.debug("Trakt data updated")

async_dispatcher_send(self.hass, DATA_UPDATED)

async def async_setup(self):
"""Set up Trakt Data."""
await self.async_add_options()
trakt.core.OAUTH_TOKEN = self.session.token[CONF_ACCESS_TOKEN]
trakt.core.CLIENT_ID = self.hass.data[DATA_TRAKT_CRED][CONF_CLIENT_ID]
trakt.core.CLIENT_SECRET = self.hass.data[DATA_TRAKT_CRED][CONF_CLIENT_SECRET]

try:
await self.async_update()
except trakt.errors.OAuthException:
_LOGGER.error(
"Trakt api is unauthrized. Please remove the entry and reconfigure."
)
return False

await self.async_set_scan_interval(
self.config_entry.options[CONF_SCAN_INTERVAL]
)
self.config_entry.add_update_listener(self.async_options_updated)

return True

async def async_add_options(self):
"""Add options for entry."""
if not self.config_entry.options:

options = {
CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL,
CONF_DAYS: DEFAULT_DAYS,
CONF_EXCLUDE: None,
}

self.hass.config_entries.async_update_entry(
self.config_entry, options=options
)

async def async_set_scan_interval(self, scan_interval):
"""Update scan interval."""

if self.unsub_timer is not None:
self.unsub_timer()
self.unsub_timer = async_track_time_interval(
self.hass, self.async_update, timedelta(minutes=scan_interval)
)

@staticmethod
async def async_options_updated(hass, entry):
"""Triggered by config entry options updates."""
await hass.data[DOMAIN].async_set_scan_interval(
entry.options[CONF_SCAN_INTERVAL]
)
await hass.data[DOMAIN].async_update()


def days_until(date):
"""Calculate days until."""
show_date = dt_util.as_local(date)
now = dt_util.as_local(dt_util.now())
return int((show_date - now).total_seconds() / 86400)
61 changes: 61 additions & 0 deletions custom_components/trakt/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Config flow for Trakt."""
import logging
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_EXCLUDE, CONF_SCAN_INTERVAL
from homeassistant.core import callback
from homeassistant.helpers import config_entry_oauth2_flow
from .const import CONF_DAYS, DEFAULT_DAYS, DEFAULT_SCAN_INTERVAL, DOMAIN


class TraktOAuth2FlowHandler(
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
):
"""Handle a Trakt config flow."""

DOMAIN = DOMAIN
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

@property
def logger(self) -> logging.Logger:
"""Return logger."""
return logging.getLogger(__name__)

@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return TraktOptionsFlowHandler(config_entry)


class TraktOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Trakt options."""

def __init__(self, config_entry):
"""Initialize Trakt options flow."""
self.config_entry = config_entry

async def async_step_init(self, user_input=None):
"""Manage the Trakt options."""
if user_input is not None:
user_input[CONF_EXCLUDE] = user_input[CONF_EXCLUDE].split(",")
return self.async_create_entry(title="", data=user_input)

options = {
vol.Optional(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
),
): int,
vol.Optional(
CONF_DAYS,
default=self.config_entry.options.get(CONF_DAYS, DEFAULT_DAYS),
): int,
vol.Optional(
CONF_EXCLUDE, default=self.config_entry.options.get(CONF_EXCLUDE, None),
): str,
}

return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
26 changes: 26 additions & 0 deletions custom_components/trakt/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Constants used in the Trakt integration."""
DOMAIN = "trakt"

OAUTH2_AUTHORIZE = "https://api-v2launch.trakt.tv/oauth/authorize"
OAUTH2_TOKEN = "https://api-v2launch.trakt.tv/oauth/token"

ATTRIBUTION = "Data provided by trakt.tv"

CONF_DAYS = "days"
CONF_EXCLUDE = "exclude"

DATA_UPDATED = "trakt_data_updated"
DATA_TRAKT_CRED = "trakt_credentials"

DEFAULT_DAYS = 30
DEFAULT_SCAN_INTERVAL = 60
DEFAULT_NAME = "Trakt Upcoming Calendar"

CARD_DEFAULT = {
"title_default": "$title",
"line1_default": "$episode",
"line2_default": "$release",
"line3_default": "$rating - $runtime",
"line4_default": "$number - $studio",
"icon": "mdi:arrow-down-bold",
}
Loading

0 comments on commit 5d3dbd4

Please sign in to comment.