|
| 1 | +"""Sonarr2 integration for Mediarr.""" |
| 2 | +from ..common.sensor import MediarrSensor |
| 3 | +from datetime import datetime, timedelta |
| 4 | +import async_timeout |
| 5 | +from zoneinfo import ZoneInfo |
| 6 | +import logging |
| 7 | + |
| 8 | + |
| 9 | +_LOGGER = logging.getLogger(__name__) |
| 10 | + |
| 11 | +class Sonarr2MediarrSensor(MediarrSensor): |
| 12 | + def __init__(self, session, api_key, url, max_items, days_to_check): |
| 13 | + """Initialize the sensor.""" |
| 14 | + self._session = session |
| 15 | + self._sonarr2_api_key = api_key |
| 16 | + self._url = url.rstrip('/') |
| 17 | + self._max_items = max_items |
| 18 | + self._days_to_check = days_to_check |
| 19 | + self._name = "Sonarr2 Mediarr" |
| 20 | + self._state = 0 |
| 21 | + |
| 22 | + |
| 23 | + |
| 24 | + @property |
| 25 | + def name(self): |
| 26 | + """Return the name of the sensor.""" |
| 27 | + return self._name |
| 28 | + |
| 29 | + @property |
| 30 | + def unique_id(self): |
| 31 | + """Return a unique ID.""" |
| 32 | + return f"sonarr2_mediarr_{self._url}" |
| 33 | + |
| 34 | + def _format_date(self, date_str: str) -> str: |
| 35 | + """Format date string.""" |
| 36 | + try: |
| 37 | + return datetime.strptime(date_str, '%Y-%m-%d').strftime('%Y-%m-%d') |
| 38 | + except ValueError: |
| 39 | + return 'Unknown' |
| 40 | + |
| 41 | + async def async_update(self): |
| 42 | + """Update the sensor.""" |
| 43 | + try: |
| 44 | + headers = {'X-Api-Key': self._sonarr2_api_key} |
| 45 | + now = datetime.now(ZoneInfo('UTC')) |
| 46 | + params = { |
| 47 | + 'start': now.strftime('%Y-%m-%d'), |
| 48 | + 'end': (now + timedelta(days=self._days_to_check)).strftime('%Y-%m-%d'), |
| 49 | + 'includeSeries': 'true' |
| 50 | + } |
| 51 | + |
| 52 | + async with async_timeout.timeout(10): |
| 53 | + async with self._session.get( |
| 54 | + f"{self._url}/api/v3/calendar", |
| 55 | + headers=headers, |
| 56 | + params=params |
| 57 | + ) as response: |
| 58 | + if response.status == 200: |
| 59 | + upcoming_episodes = await response.json() |
| 60 | + card_json = [] |
| 61 | + shows_dict = {} |
| 62 | + |
| 63 | + for episode in upcoming_episodes: |
| 64 | + series = episode.get('series', {}) |
| 65 | + air_date = self._format_date(episode['airDate']) |
| 66 | + |
| 67 | + if air_date == 'Unknown': |
| 68 | + continue |
| 69 | + |
| 70 | + series_id = series['id'] |
| 71 | + images = {img['coverType']: img['remoteUrl'] for img in series.get('images', [])} |
| 72 | + |
| 73 | + show_data = { |
| 74 | + 'title': f"{series['title']} - {episode.get('seasonNumber', 0):02d}x{episode.get('episodeNumber', 0):02d}", |
| 75 | + 'episode': str(episode.get('title', 'Unknown')), |
| 76 | + 'release': air_date, |
| 77 | + 'number': f"S{episode.get('seasonNumber', 0):02d}E{episode.get('episodeNumber', 0):02d}", |
| 78 | + 'runtime': str(series.get('runtime', 0)), |
| 79 | + 'network': str(series.get('network', 'N/A')), |
| 80 | + 'poster': images.get('poster', ''), |
| 81 | + 'fanart': images.get('fanart', ''), |
| 82 | + 'banner': images.get('banner', ''), |
| 83 | + 'season': str(episode.get('seasonNumber', 0)), |
| 84 | + 'details': f"{series['title']}\n{episode.get('title', 'Unknown')}\nS{episode.get('seasonNumber', 0):02d}E{episode.get('episodeNumber', 0):02d}", |
| 85 | + 'flag': 1 |
| 86 | + } |
| 87 | + |
| 88 | + # Take earliest episode air date for each series |
| 89 | + if series_id not in shows_dict or air_date < shows_dict[series_id]['release']: |
| 90 | + shows_dict[series_id] = show_data |
| 91 | + |
| 92 | + upcoming_shows = list(shows_dict.values()) |
| 93 | + upcoming_shows.sort(key=lambda x: x['release']) |
| 94 | + card_json.extend(upcoming_shows[:self._max_items]) |
| 95 | + |
| 96 | + if not card_json: |
| 97 | + card_json.append({ |
| 98 | + 'title_default': '$title', |
| 99 | + 'line1_default': '$episode', |
| 100 | + 'line2_default': '$release', |
| 101 | + 'line3_default': '$number', |
| 102 | + 'line4_default': '$runtime - $network', |
| 103 | + 'icon': 'mdi:arrow-down-circle' |
| 104 | + }) |
| 105 | + |
| 106 | + self._state = len(upcoming_shows) |
| 107 | + self._attributes = {'data': card_json} |
| 108 | + self._available = True |
| 109 | + else: |
| 110 | + raise Exception(f"Failed to connect to Sonarr2. Status: {response.status}") |
| 111 | + |
| 112 | + except Exception as err: |
| 113 | + _LOGGER.error("Error updating Sonarr2 sensor: %s", err) |
| 114 | + self._state = 0 |
| 115 | + self._attributes = {'data': []} |
| 116 | + self._available = False |
0 commit comments