From ef6e88b576b0fd152a823831f610498a4481debb Mon Sep 17 00:00:00 2001 From: Benjamin Krause Date: Wed, 14 Oct 2020 11:02:41 +0200 Subject: [PATCH] Initial commit --- README.md | 78 ++++++++ medusa/custom_components/medusa/__init__.py | 0 medusa/custom_components/medusa/manifest.json | 8 + medusa/custom_components/medusa/sensor.py | 185 ++++++++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 README.md create mode 100644 medusa/custom_components/medusa/__init__.py create mode 100644 medusa/custom_components/medusa/manifest.json create mode 100644 medusa/custom_components/medusa/sensor.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..1049f8f --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +Medusa Upcoming TV Shows +============ +A Home Assistant sensor that pulls the latest upcoming TV shows from Medusa. This is a fork of Youdroid's [SickChill Wanted Tv Shows](https://github.com/youdroid/home-assistant-sickchill) with a few fixes such as compability with the awesome [Flex Table](https://github.com/custom-cards/flex-table-card) and better airdate handling. The sensor should work with any Sick* fork (SickChill, SickRage, etc.). + +## Installation using HACS (Recommended) +1. Navigate to HACS and add a custom repository + **URL:** https://git.idmedia.no/home-assistant/hass-medusa + **Category:** Integration +2. Install module as usual +3. Restart Home Assistant + +## Configuration +The sensor is compatible with [Upcoming Media Card](https://github.com/custom-cards/upcoming-media-card) and [Flex Table](https://github.com/custom-cards/flex-table-card) so you may install either depending on your preferences. + +| Key | Default | Required | Description +| --- | --- | --- | --- +| token | | yes | Your Medusa token (Config > General > Interface > API Key > Generate) +| name | medusa | no | Name of the sensor. +| host | localhost | no | The host which Medusa is running on. +| port | 8081 | no | The port which Medusa is running on. +| protocol | http | no | The HTTP protocol used by Medusa. +| sort | name | no | Parameter to sort TV Shows **[date, name]** +| webroot | | no | WebRoot parameter if you change it in config.ini (Syntax : **/newWebRoot**) + +## Example +Add the following to your `configuration.yaml`: +``` +sensor: + - platform: medusa + name: medusa + host: !secret medusa_host + token: !secret medusa_token + sort: date +``` + +Add the following to your `lovelace.yaml`: +``` +- type: 'custom:flex-table-card' + title: Upcoming TV Shows + clickable: false + max_rows: 5 + strict: true + entities: + include: sensor.medusa + columns: + - data: data + name: ' ' + modify: >- + x.poster ? '' : + ''; + - data: data + name: ' ' + modify: >- + const hourDiff = (Date.parse(x.airdate) - Date.now()); + const secDiff = hourDiff / 1000; + const minDiff = hourDiff / 60 / 1000; + const hDiff = hourDiff / 3600 / 1000; + const dDiff = hourDiff / 3600 / 1000 / 24; + const days = Math.floor(dDiff); + const hours = Math.floor(hDiff - (days * 24)); + const minutes = Math.floor(minDiff - 60 * Math.floor(hDiff)); + const tdays = (Math.abs(days) > 1) ? days + " days " : ((Math.abs(days) == 1) ? days + " day " : ""); + const thours = (Math.abs(hours) > 1) ? hours + " hours " : ((Math.abs(hours) == 1) ? hours + " hour " : ""); + const tminutes = (Math.abs(minutes) > 1) ? minutes + " minutes " : ((Math.abs(minutes) == 1) ? minutes + " minute " : ""); + const episodeNumber = x.number ? x.number : ''; + const episodeTitle = x.episode ? x.episode : ''; + const title = x.title ? x.title : ''; + const subTitle = [episodeNumber, episodeTitle].filter(Boolean).join(" - "); + const timeLeft = (hourDiff > 0) ? tdays + thours + tminutes : 'Not downloaded yet'; + + if (title) { + "" + title + "
" + + subTitle + "
" + + timeLeft + } else { + null; + } +``` \ No newline at end of file diff --git a/medusa/custom_components/medusa/__init__.py b/medusa/custom_components/medusa/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/medusa/custom_components/medusa/manifest.json b/medusa/custom_components/medusa/manifest.json new file mode 100644 index 0000000..abece23 --- /dev/null +++ b/medusa/custom_components/medusa/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "medusa", + "name": "Medusa Upcoming TV Shows", + "documentation": "https://github.com/IDmedia/hass-medusa", + "requirements": [], + "dependencies": [], + "codeowners": ["@IDmedia"] +} diff --git a/medusa/custom_components/medusa/sensor.py b/medusa/custom_components/medusa/sensor.py new file mode 100644 index 0000000..e62db93 --- /dev/null +++ b/medusa/custom_components/medusa/sensor.py @@ -0,0 +1,185 @@ +"""Platform for sensor integration.""" +import json +import logging +import os +import re +from datetime import datetime +import homeassistant.helpers.config_validation as cv +import requests +import voluptuous as vol +from homeassistant.components.switch import PLATFORM_SCHEMA +from homeassistant.const import * +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) +DEFAULT_NAME = "medusa" +DEFAULT_HOST = "localhost" +DEFAULT_PROTO = "http" +DEFAULT_PORT = "8081" +DEFAULT_SORTING = "name" +CONF_SORTING = "sort" +CONF_WEB_ROOT = "webroot" +DEFAULT_WEB_ROOT = "" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_TOKEN): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTO): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_SORTING, default=DEFAULT_SORTING): cv.string, + vol.Optional(CONF_WEB_ROOT, default=DEFAULT_WEB_ROOT): cv.string +}) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + add_entities([MedusaSensor(config, hass)]) + + +class MedusaSensor(Entity): + """Representation of a Sensor.""" + + def __init__(self, config, hass): + self._state = None + self._name = config.get(CONF_NAME) + self.token = config.get(CONF_TOKEN) + self.host = config.get(CONF_HOST) + self.protocol = config.get(CONF_PROTOCOL) + self.port = config.get(CONF_PORT) + self.base_dir = str(hass.config.path()) + '/' + self.data = None + self.sort = config.get(CONF_SORTING) + self.web_root = config.get(CONF_WEB_ROOT) + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self.data + + def update(self): + attributes = {} + card_json = [] + card_shows = [] + init = {} + """Initialized JSON Object""" + init['title_default'] = '$title' + init['line1_default'] = '$episode' + init['line2_default'] = '$release' + init['line3_default'] = '$number - $rating - $runtime' + init['line4_default'] = '$genres' + init['icon'] = 'mdi:eye-off' + card_json.append(init) + + tv_shows = self.get_infos(self.protocol, self.host, self.port, self.token, self.web_root, 'future') + + directory = "{0}/www/custom-lovelace/{1}/images/".format(self.base_dir, self._name) + if not os.path.exists(directory): + os.makedirs(directory) + regex_img = re.compile(r'\d+-(fanart|poster)\.jpg') + lst_images = list(filter(regex_img.search, + os.listdir(directory))) + + for category in tv_shows["data"]: + for show in tv_shows["data"].get(category): + + airdate_str = show["airdate"] + ' ' + show["airs"] + airdate_dt = datetime.strptime(airdate_str, "%Y-%m-%d %A %I:%M %p") + airdate = airdate_dt.strftime("%Y-%m-%d %H:%M:%SZ") + + number = "S" + str(show["season"]).zfill(2) + "E" + str(show["episode"]).zfill(2) + + banner = "{0}-banner.jpg".format(show["indexerid"]) + fanart = "{0}-fanart.jpg".format(show["indexerid"]) + poster = "{0}-poster.jpg".format(show["indexerid"]) + + card_items = {} + card_items["airdate"] = airdate + card_items["number"] = number + card_items["category"] = category + card_items["studio"] = show["network"] + card_items["title"] = show["show_name"] + card_items["episode"] = show["ep_name"] + card_items["release"] = '$day, $date $time' + card_items["poster"] = self.add_poster(lst_images, directory, poster, show["indexerid"], card_items) + card_items["fanart"] = self.add_fanart(lst_images, directory, fanart, show["indexerid"], card_items) + card_items["banner"] = self.add_banner(lst_images, directory, banner, show["indexerid"], card_items) + + card_shows.append(card_items) + + if self.sort == "date": + card_shows.sort(key=lambda x: x.get("airdate")) + card_json = card_json + card_shows + attributes["data"] = card_json + self._state = tv_shows["result"] + self.data = attributes + self.delete_old_tvshows(lst_images, directory) + + def get_infos(self, proto, host, port, token, web_root, cmd): + url = "{0}://{1}:{2}{3}/api/{4}/?cmd={5}".format( + proto, host, port, web_root, token, cmd) + ifs_movies = requests.get(url).json() + return ifs_movies + + def add_poster(self, lst_images, directory, poster, id, card_items): + if poster in lst_images: + lst_images.remove(poster) + else: + img_data = requests.get("{0}://{1}:{2}{3}/api/v1/{4}/?cmd=show.getposter&indexerid={5}".format(self.protocol, self.host, self.port, self.web_root, self.token, id)) + + if not img_data.status_code.__eq__(200): + _LOGGER.error(card_items) + return "" + + try: + open(directory + poster, 'wb').write(img_data.content) + except IOError: + _LOGGER.error("Unable to create file.") + return "/local/custom-lovelace/{0}/images/{1}".format(self._name, poster) + + def add_fanart(self, lst_images, directory, fanart, id, card_items): + if fanart in lst_images: + lst_images.remove(fanart) + else: + img_data = requests.get("{0}://{1}:{2}{3}/api/v1/{4}/?cmd=show.getfanart&indexerid={5}".format(self.protocol, self.host, self.port, self.web_root, self.token, id)) + + if not img_data.status_code.__eq__(200): + return "" + + try: + open(directory + fanart, 'wb').write(img_data.content) + except IOError: + _LOGGER.error("Unable to create file.") + return "/local/custom-lovelace/{0}/images/{1}".format(self._name, fanart) + + def add_banner(self, lst_images, directory, banner, id, card_items): + if banner in lst_images: + lst_images.remove(banner) + else: + img_data = requests.get("{0}://{1}:{2}/api/v1/{3}/?cmd=show.getbanner&indexerid={4}".format(self.protocol, self.host, self.port, self.token, id)) + + if not img_data.status_code.__eq__(200): + return "" + + try: + open(directory + banner, 'wb').write(img_data.content) + except IOError: + _LOGGER.error("Unable to create file.") + return "/local/custom-lovelace/{0}/images/{1}".format(self._name, banner) + + def delete_old_tvshows(self, lst_images, directory): + for img in lst_images: + try: + os.remove(directory + img) + _LOGGER.info("Delete finished tv show images") + except IOError: + _LOGGER.error("Unable to delete file.") \ No newline at end of file