# -*- coding: utf-8 -*-
"""My movies / My series as native Videos window listings (docs/plan/milionar-library/060).

Imported through the main.py fast path (`action=mlist`, `action=mupcoming`): no router import, so a listing opens in
~100 ms. Data comes from the store (lists, watch state) and the `meta` cache; only a season never seen before is
fetched once (030 §3). Items play through the canonical URLs (040 §2) and carry the state of the store.
"""
import time
from datetime import date
from urllib.parse import quote, quote_plus

import xbmc
import xbmcaddon
import xbmcgui
import xbmcplugin

from wcs.library2 import clicks, menu, meta, next_up, rows, store, tmdb, urls

ADDON_ID = 'plugin.video.milionar'
BASE = 'plugin://{}/'.format(ADDON_ID)
POSTER = 'https://image.tmdb.org/t/p/w500'
BACKDROP = 'https://image.tmdb.org/t/p/w1280'
PROFILE_IMG = 'https://image.tmdb.org/t/p/w185'
STRING_SPECIALS, STRING_AIRS, STRING_EMPTY, STRING_SEASON = 32290, 32291, 32292, 32293
# the old My list screens' own strings (kept for the top buttons): label, plot
STRINGS_ADD = {'movies': (31118, 31119), 'series': (31035, 31036)}
STRINGS_DELETE_ALL = {'movies': (31120, 31121), 'series': (31037, 31038)}
AI_LABEL = 'Poradit se s AI (beta)'
AI_PLOT = 'BETA: Tato funkce není hotová a nemusí dávat 100% smysl. Spustit interaktivní AI chat s ohledem na aktuální kontext.'


def _text(string_id):
    return xbmcaddon.Addon(ADDON_ID).getLocalizedString(string_id)


def _stamp(epoch):
    return time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(epoch)) if epoch else ''


def _image(base, path):
    if not path:
        return ''
    return base + path if path.startswith('/') and not path.startswith('/Users') else path


def _logo(tmdb_id, media_type):
    """clearlogo of a title when it is already cached (never downloads here)."""
    try:
        from wcs.metadata import TMDbCache
        endpoint = '{}/{}/images'.format('tv' if media_type == 'tv' else 'movie', tmdb_id)
        data = TMDbCache.get_from_cache(endpoint, {'kind': 'logo'}, 7 * 24 * 3600)
        return data.get('url', '') if data else ''
    except Exception:
        return ''


def _logos(ids, media_type):
    """The same logo TTL as _logo(), with one connection for the entire listing."""
    import json
    from wcs.metadata import TMDbCache
    keys = {TMDbCache._make_key('{}/{}/images'.format(media_type, i), {'kind': 'logo'}): i for i in ids}
    if not keys:
        return {}
    conn = None
    try:
        conn = store.connect()
        result, wanted, now = {}, list(keys), time.time()
        for start in range(0, len(wanted), 500):
            chunk = wanted[start:start + 500]
            for row in conn.execute('SELECT key,json,fetched_at FROM meta WHERE key IN ({})'.format(
                    ','.join('?' for _ in chunk)), chunk):
                if int(now) - row['fetched_at'] <= 7 * 24 * 3600:
                    result[keys[row['key']]] = (json.loads(row['json']) or {}).get('url', '')
        return result
    except Exception:
        return {}
    finally:
        if conn is not None:
            conn.close()


def _names(entries):
    return [e.get('name', '') for e in entries or [] if e.get('name')]


def _cast(credits):
    cast = []
    for order, person in enumerate(((credits or {}).get('cast') or [])[:20]):
        cast.append(xbmc.Actor(person.get('name', ''), person.get('character', ''), order,
                               _image(PROFILE_IMG, person.get('profile_path'))))
    return cast


def _crew(credits, *jobs):
    return [c.get('name', '') for c in ((credits or {}).get('crew') or []) if c.get('job') in jobs and c.get('name')]


def _state_on(tag, state):
    """Playcount, resume point and last played of a store state row."""
    if not state:
        return
    tag.setPlaycount(int(state.get('playcount') or 0))
    if state.get('resume_pos'):
        tag.setResumePoint(float(state['resume_pos']), float(state.get('resume_total') or 0))
    if state.get('last_played_at'):
        tag.setLastPlayed(_stamp(state['last_played_at']))


def _props(item, kind, tmdb_id, season=None, episode=None):
    item.setProperty('milionar.type', kind)
    item.setProperty('milionar.tmdb', str(tmdb_id))
    if season is not None:
        item.setProperty('milionar.s', str(season))
    if episode is not None:
        item.setProperty('milionar.e', str(episode))


def _finish(handle, items, content, sorts, category=''):
    if category:
        xbmcplugin.setPluginCategory(handle, category)
    xbmcplugin.setContent(handle, content)
    for method in sorts:
        xbmcplugin.addSortMethod(handle, method)
    xbmcplugin.addDirectoryItems(handle, items, len(items))
    xbmcplugin.endOfDirectory(handle, succeeded=True, updateListing=False, cacheToDisc=False)


def _empty_item():
    """060 §4: one item that opens the search."""
    label = _text(STRING_EMPTY)
    item = xbmcgui.ListItem(offscreen=True, label=label, path='{}?action=skin_run&run={}'.format(
        BASE, quote('action=skin_search&query=', safe='')))
    item.setProperty('IsPlayable', 'true')
    item.getVideoInfoTag().setTitle(label)
    return item.getPath(), item, False


def _button(label, plot, path, folder=False, icon='DefaultAddSource.png'):
    item = xbmcgui.ListItem(offscreen=True, label=label, path=path)
    item.setArt({'icon': icon})
    tag = item.getVideoInfoTag()
    tag.setTitle(label)
    tag.setPlot(plot)
    item.setProperty('SpecialSort', 'top')             # stays on top whatever the sort method is
    return path, item, folder


def _top_buttons(kind, has_titles):
    """Buttons the old My list screens had on top (user, 2026-10-01): AI chat, add, delete all (red, with a confirm in
    the old action)."""
    addon = xbmcaddon.Addon(ADDON_ID)
    media = 'movie' if kind == 'movies' else 'tv'
    buttons = []
    if addon.getSetting('menu_show_ai_chat') == 'true':
        buttons.append(_button(AI_LABEL, AI_PLOT, '{}?action=ai_chat_recommendation&context_type={}&media_type={}'.format(
            BASE, 'my_movies' if kind == 'movies' else 'my_series', media), folder=True, icon='DefaultMusicSearch.png'))
    label_id, plot_id = STRINGS_ADD[kind]
    buttons.append(_button(_text(label_id), _text(plot_id), urls.add_url(kind)))
    if has_titles:
        label_id, plot_id = STRINGS_DELETE_ALL[kind]
        buttons.append(_button('[COLOR red]{}[/COLOR]'.format(_text(label_id)), _text(plot_id),
                               '{}?action={}'.format(BASE, 'tmdb_remove_all_movies' if kind == 'movies' else 'tmdb_remove_all_series'),
                               icon='none.png'))
    return buttons


# --------------------------------------------------------------------------- movies

def movie_item(entry, data, state, times, click=None, logo=None):
    """ListItem of a movie from the list snapshot `entry`, the cached TMDb `data` (or None) and the store state.

    `click`: the skin's click (clicks.list_click); None = the classic addon menu, which keeps the old addon setting."""
    tmdb_id = int(entry['id'])
    data = data or {}
    title = data.get('title') or entry.get('title') or entry.get('name') or str(tmdb_id)
    date_text = data.get('release_date') or ''
    year = (date_text[:4] or str(entry.get('year') or ''))
    if click:
        path, _folder = clicks.title_path('movie', tmdb_id, click, meta={
            'title': title, 'original': data.get('original_title') or title,
            'plot': data.get('overview') or entry.get('overview') or '',
            'poster': _image(POSTER, data.get('poster_path') or entry.get('poster_path') or entry.get('poster'))})
        item = xbmcgui.ListItem(offscreen=True, label=title, path=path)
        item.setProperty('IsPlayable', 'true')
    elif xbmcaddon.Addon(ADDON_ID).getSetting('default_movie_action') == '0':      # "Zobrazit detaily" (old setting)
        item = xbmcgui.ListItem(offscreen=True, label=title, path='{}?action=show_wcs_details&media_type=movie&tmdb_id={}'.format(BASE, tmdb_id))
    else:
        item = xbmcgui.ListItem(offscreen=True, label=title, path=urls.movie_url(tmdb_id))
        item.setProperty('IsPlayable', 'true')
    tag = item.getVideoInfoTag()
    tag.setMediaType('movie')
    tag.setTitle(title)
    if data.get('original_title'):
        tag.setOriginalTitle(data['original_title'])
    if year.isdigit():
        tag.setYear(int(year))
    if date_text:
        tag.setPremiered(date_text)
    tag.setPlot(data.get('overview') or entry.get('overview') or '')
    if data.get('tagline'):
        tag.setTagLine(data['tagline'])
    rating = data.get('vote_average') or entry.get('vote_average') or 0
    if rating:
        tag.setRating(float(rating), int(data.get('vote_count') or 0), 'tmdb', True)
    tag.setGenres(_names(data.get('genres')))
    tag.setStudios(_names(data.get('production_companies')))
    tag.setCountries(_names(data.get('production_countries')))
    if data.get('runtime'):
        tag.setDuration(int(data['runtime']) * 60)
    credits = data.get('credits')
    tag.setCast(_cast(credits))
    tag.setDirectors(_crew(credits, 'Director'))
    tag.setWriters(_crew(credits, 'Writer', 'Screenplay'))
    tag.setUniqueIDs({'tmdb': str(tmdb_id)}, 'tmdb')
    if times.get('added'):
        tag.setDateAdded(_stamp(times['added']))
    _state_on(tag, state)
    poster = _image(POSTER, data.get('poster_path') or entry.get('poster_path') or entry.get('poster'))
    fanart = _image(BACKDROP, data.get('backdrop_path') or entry.get('backdrop_path'))
    art = {'poster': poster, 'thumb': poster, 'icon': poster, 'fanart': fanart or poster, 'landscape': fanart,
           'clearlogo': _logo(tmdb_id, 'movie') if logo is None else logo}
    rows._set_art(item, art)
    _props(item, 'movie', tmdb_id)
    return item


def movies(handle, genre=None, skin=False):
    data = rows.Data()
    entries = data.entries('movie')
    cached = data.meta_many(meta.key_movie(int(e['id'])) for e in entries)
    if genre:
        entries = [e for e in entries if _has_genre(cached.get(meta.key_movie(int(e['id']))), genre)]
    items = [] if genre else _top_buttons('movies', bool(entries))       # no buttons in a genre-filtered view
    if not entries:
        return _finish(handle, items + [_empty_item()], 'movies', [], 'Moje filmy')
    states, times = data.movie_states(), data.list_times('movie')
    logos = _logos([int(e['id']) for e in entries], 'movie')
    click = clicks.list_click('movies') if skin else None
    for entry in entries:
        tmdb_id = int(entry['id'])
        item = movie_item(entry, cached.get(meta.key_movie(tmdb_id)), states.get(tmdb_id), times.get(tmdb_id, {}), click,
                          logos.get(tmdb_id, ''))
        items.append((item.getPath(), item, False))
    menu.attach_items(items, data)
    _finish(handle, items, 'movies', _movie_sorts(), 'Moje filmy')


def _has_genre(data, genre):
    return any(str(g.get('id')) == str(genre) for g in (data or {}).get('genres') or [])


def _movie_sorts():
    return [xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE, xbmcplugin.SORT_METHOD_VIDEO_YEAR, xbmcplugin.SORT_METHOD_DATEADDED,
            xbmcplugin.SORT_METHOD_VIDEO_RATING, xbmcplugin.SORT_METHOD_LASTPLAYED]


# --------------------------------------------------------------------------- series, seasons, episodes

def _season_numbers(show):
    return [int(e.get('season_number', 0)) for e in show.get('seasons') or [] if int(e.get('season_number', 0)) > 0]


def _seasons_cached(show_id, show, cache=None):
    """{number: season data} of the regular seasons present in the cache (never downloads); `cache` is a
    {key: data} map from meta.get_many."""
    if cache is None:
        cache = meta.get_many(meta.key_season(show_id, n) for n in _season_numbers(show))
    seasons = {}
    for number in _season_numbers(show):
        data = cache.get(meta.key_season(show_id, number))
        if data and 'episodes' in data:
            seasons[number] = data
    return seasons


def series_counts(show, seasons, states, today, order=None):
    """(total aired regular episodes, watched among them, seasons count) - falls back to the show's own totals when
    season data is not cached yet."""
    if order is None:
        order = next_up.episode_order(show, seasons, today)
    seasons_count = len([s for s in show.get('seasons') or [] if int(s.get('season_number', 0)) > 0])
    if order and len(seasons) == seasons_count:
        aired = [ep for ep in order if ep['aired']]
        watched = sum(1 for ep in aired if int((states.get((ep['s'], ep['e'])) or {}).get('playcount') or 0) > 0)
        return len(aired), watched, seasons_count
    total = int(show.get('number_of_episodes') or 0)
    watched = sum(1 for (s, _e), st in states.items() if s > 0 and int(st.get('playcount') or 0) > 0)
    return total, min(watched, total) if total else watched, seasons_count


def series_item(entry, show, states, times, today, season_cache=None, click=None, logo=None, counts=None):
    tmdb_id = int(entry['id'])
    show = show or {}
    name = show.get('name') or entry.get('name') or entry.get('title') or str(tmdb_id)
    folder = True
    if click and click != 'folder':
        path, folder = clicks.title_path('tv', tmdb_id, click, meta={
            'title': name, 'original': show.get('original_name') or name, 'plot': show.get('overview') or entry.get('overview') or '',
            'poster': _image(POSTER, show.get('poster_path') or entry.get('poster_path') or entry.get('poster'))})
    else:
        path = '{}?action=mlist&kind=seasons&tmdb={}'.format(BASE, tmdb_id)
    item = xbmcgui.ListItem(offscreen=True, label=name, path=path)
    if not folder:
        item.setProperty('IsPlayable', 'true')
    tag = item.getVideoInfoTag()
    tag.setMediaType('tvshow')
    tag.setTitle(name)
    tag.setTvShowTitle(name)
    if show.get('original_name'):
        tag.setOriginalTitle(show['original_name'])
    first = show.get('first_air_date') or ''
    year = first[:4] or str(entry.get('year') or '')
    if year.isdigit():
        tag.setYear(int(year))
    if first:
        tag.setPremiered(first)
    tag.setPlot(show.get('overview') or entry.get('overview') or '')
    if show.get('tagline'):
        tag.setTagLine(show['tagline'])
    rating = show.get('vote_average') or entry.get('vote_average') or 0
    if rating:
        tag.setRating(float(rating), int(show.get('vote_count') or 0), 'tmdb', True)
    tag.setGenres(_names(show.get('genres')))
    tag.setStudios(_names(show.get('networks')))
    tag.setCountries(show.get('origin_country') or _names(show.get('production_countries')))
    tag.setUniqueIDs({'tmdb': str(tmdb_id)}, 'tmdb')
    if times.get('added'):
        tag.setDateAdded(_stamp(times['added']))
    if times.get('activity'):
        tag.setLastPlayed(_stamp(times['activity']))
    if counts is None:
        seasons = _seasons_cached(tmdb_id, show, season_cache) if show else {}
        counts = series_counts(show, seasons, states, today)
    total, watched, seasons_count = counts
    tag.setPlaycount(1 if total and watched >= total else 0)
    item.setProperty('TotalSeasons', str(seasons_count))
    _counts(item, total, watched)
    poster = _image(POSTER, show.get('poster_path') or entry.get('poster_path') or entry.get('poster'))
    fanart = _image(BACKDROP, show.get('backdrop_path') or entry.get('backdrop_path'))
    art = {'poster': poster, 'thumb': poster, 'icon': poster, 'fanart': fanart or poster, 'landscape': fanart,
           'clearlogo': _logo(tmdb_id, 'tv') if logo is None else logo}
    rows._set_art(item, art)
    _props(item, 'tv', tmdb_id)
    return item


def series(handle, genre=None, skin=False):
    data = rows.Data(today=date.today())
    entries = data.entries('tv')
    shows = data.meta_many(meta.key_tv(int(e['id'])) for e in entries)
    if genre:
        entries = [e for e in entries if _has_genre(shows.get(meta.key_tv(int(e['id']))), genre)]
    buttons = [] if genre else _top_buttons('series', bool(entries))
    if not entries:
        return _finish(handle, buttons + [_empty_item()], 'tvshows', [], 'Moje seriály')
    states, times, today = data.episode_states(), data.list_times('tv'), data.today
    data.preload_series([int(e['id']) for e in entries])
    seasons = data.meta_many(meta.key_season(int(e['id']), n) for e in entries
                            for n in _season_numbers(shows.get(meta.key_tv(int(e['id'])), {})))
    logos = _logos([int(e['id']) for e in entries], 'tv')
    items = list(buttons)
    click = clicks.list_click('series') if skin else None
    for entry in entries:
        tmdb_id = int(entry['id'])
        show, order, _state = data.series_state(tmdb_id)
        counts = series_counts(show, _seasons_cached(tmdb_id, show, seasons), states.get(tmdb_id, {}), today, order)
        item = series_item(entry, shows.get(meta.key_tv(tmdb_id)), states.get(tmdb_id, {}), times.get(tmdb_id, {}), today,
                           seasons, click, logos.get(tmdb_id, ''), counts)
        items.append((item.getPath(), item, item.getProperty('IsPlayable') != 'true'))
    menu.attach_items(items, data)
    _finish(handle, items, 'tvshows', [xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE, xbmcplugin.SORT_METHOD_VIDEO_YEAR,
                                       xbmcplugin.SORT_METHOD_DATEADDED, xbmcplugin.SORT_METHOD_LASTPLAYED], 'Moje seriály')


def season_items(show_id, show, states, today, cache=None):
    """Season folders like the Kodi library: "* Všechny sezóny" first, regular seasons 1..N, then the specials
    (season 0) when TMDb lists any. Every folder carries watched / total episode counts."""
    entries = sorted(show.get('seasons') or [], key=lambda s: (int(s.get('season_number', 0)) == 0, int(s.get('season_number', 0))))
    name = show.get('name', '')
    fanart = _image(BACKDROP, show.get('backdrop_path'))
    show_poster = _image(POSTER, show.get('poster_path'))
    if cache is None:
        cache = meta.get_many(meta.key_season(show_id, int(e.get('season_number', 0))) for e in entries)
    items, all_total, all_watched = [], 0, 0
    for entry in entries:
        number = int(entry.get('season_number', 0))
        if number == 0 and not int(entry.get('episode_count') or 0):
            continue
        label = _text(STRING_SPECIALS) if number == 0 else (entry.get('name') or '{} {}'.format(_text(STRING_SEASON), number))
        item = xbmcgui.ListItem(offscreen=True, label=label, path='{}?action=mlist&kind=episodes&tmdb={}&s={}'.format(BASE, show_id, number))
        tag = item.getVideoInfoTag()
        tag.setMediaType('season')
        tag.setTitle(label)
        tag.setTvShowTitle(name)
        tag.setSeason(number)
        tag.setPlot(show.get('overview') or '')                                  # the library shows the series plot here
        if entry.get('air_date'):
            tag.setPremiered(entry['air_date'])
        data = cache.get(meta.key_season(show_id, number))
        if data and 'episodes' in data:
            aired = [e for e in data['episodes'] if _aired(e, today)]
            total = len(aired)
            watched = sum(1 for e in aired if int((states.get((number, e.get('episode_number'))) or {}).get('playcount') or 0) > 0)
        else:
            total = int(entry.get('episode_count') or 0)
            watched = sum(1 for (s, _e), st in states.items() if s == number and int(st.get('playcount') or 0) > 0)
        all_total += total
        all_watched += watched
        tag.setPlaycount(1 if total and watched >= total else 0)
        _counts(item, total, watched)
        poster = _image(POSTER, entry.get('poster_path')) or show_poster
        rows._set_art(item, {'poster': poster, 'thumb': poster, 'icon': poster,
                             'fanart': fanart or show_poster, 'tvshow.poster': show_poster})
        _props(item, 'season', show_id, season=number)
        items.append((item.getPath(), item, True))
    if len(items) > 1 or items:
        label = xbmc.getLocalizedString(20366)                                    # "* Všechny sezóny" (Kodi's own string)
        item = xbmcgui.ListItem(offscreen=True, label=label, path='{}?action=mlist&kind=episodes&tmdb={}&s=all'.format(BASE, show_id))
        tag = item.getVideoInfoTag()
        tag.setMediaType('season')
        tag.setTitle(label)
        tag.setTvShowTitle(name)
        tag.setPlot(show.get('overview') or '')
        tag.setSeason(-1)
        tag.setPlaycount(1 if all_total and all_watched >= all_total else 0)
        _counts(item, all_total, all_watched)
        rows._set_art(item, {'poster': show_poster, 'thumb': show_poster, 'icon': show_poster,
                             'fanart': fanart or show_poster, 'tvshow.poster': show_poster})
        _props(item, 'season', show_id, season=-1)
        items.insert(0, (item.getPath(), item, True))
    return items


def _counts(item, total, watched):
    for key, value in (('TotalEpisodes', total), ('NumEpisodes', total), ('WatchedEpisodes', watched),
                       ('UnWatchedEpisodes', max(total - watched, 0))):
        item.setProperty(key, str(value))


def _aired(episode, today):
    air = next_up._parse_date(episode.get('air_date'))
    return bool(air and air <= today)


def seasons(handle, params):
    show_id = int(params['tmdb'])
    show = tmdb.show(show_id) or {}
    if not show:
        return _finish(handle, [], 'seasons', [])
    data = rows.Data(today=date.today())
    data._cache[meta.key_tv(show_id)] = show
    cache = data.meta_many(meta.key_season(show_id, int(e.get('season_number', 0))) for e in show.get('seasons') or [])
    items = season_items(show_id, show, data.episode_states().get(show_id, {}), data.today, cache)
    menu.attach_items(items, data)
    _finish(handle, items, 'seasons', [xbmcplugin.SORT_METHOD_UNSORTED], 'Moje seriály / {}'.format(show.get('name', '')))


def episode_item(show_id, show, number, episode, state, today, all_seasons=False):
    ep_no = int(episode.get('episode_number', 0))
    name = episode.get('name') or 'Epizoda {}'.format(ep_no)
    air = next_up._parse_date(episode.get('air_date'))
    aired = bool(air and air <= today)
    label = '{}x{:02d}. {}'.format(number, ep_no, name) if all_seasons else '{:02d}. {}'.format(ep_no, name)   # like the library
    if aired:
        item = xbmcgui.ListItem(offscreen=True, label=label, path=urls.episode_url(show_id, number, ep_no))
        item.setProperty('IsPlayable', 'true')
    else:
        when = next_up.air_label(episode.get('air_date')) or '?'
        label = '{} ({})'.format(label, _text(STRING_AIRS).format(when))
        item = xbmcgui.ListItem(offscreen=True, label=label, path='{}?action=mupcoming&text={}'.format(
            BASE, quote_plus('S{:02d}E{:02d} {}'.format(number, ep_no, _text(STRING_AIRS).format(when)))))
        item.setProperty('IsPlayable', 'true')            # resolves to nothing and shows the date
    tag = item.getVideoInfoTag()
    tag.setMediaType('episode')
    tag.setTitle(label)                      # Kodi builds the row text from the title: "01. Název" like the library
    tag.setTvShowTitle(show.get('name', ''))
    tag.setSeason(number)
    tag.setEpisode(ep_no)
    tag.setPlot(episode.get('overview') or '')
    if episode.get('air_date'):
        tag.setFirstAired(episode['air_date'])
    rating = episode.get('vote_average') or 0
    if rating:
        tag.setRating(float(rating), int(episode.get('vote_count') or 0), 'tmdb', True)
    if episode.get('runtime'):
        tag.setDuration(int(episode['runtime']) * 60)
    guests = [{'name': g.get('name', ''), 'character': g.get('character', '')} for g in (episode.get('guest_stars') or [])[:10]]
    tag.setCast([xbmc.Actor(g['name'], g['character'], i, '') for i, g in enumerate(guests)])
    tag.setDirectors(_crew({'crew': episode.get('crew')}, 'Director'))
    tag.setWriters(_crew({'crew': episode.get('crew')}, 'Writer', 'Screenplay'))
    tag.setUniqueIDs({'tmdb': str(episode.get('id', ''))}, 'tmdb')
    if aired:
        _state_on(tag, state)
    still = _image(POSTER, episode.get('still_path'))
    poster = _image(POSTER, show.get('poster_path'))
    fanart = _image(BACKDROP, show.get('backdrop_path'))
    # no 'poster': Estuary then shows the episode still in the info panel, as it does for library episodes
    art = {'thumb': still or poster, 'icon': still or poster, 'fanart': fanart or poster,
           'landscape': still, 'tvshow.poster': poster}
    rows._set_art(item, art)
    _props(item, 'episode', show_id, season=number, episode=ep_no)
    return item


def episodes(handle, params):
    show_id = int(params['tmdb'])
    show = tmdb.show(show_id) or {}
    model = rows.Data(today=date.today())
    model._cache[meta.key_tv(show_id)] = show
    states = model.episode_states().get(show_id, {})
    today = model.today
    everything = params.get('s') == 'all'
    if everything:
        numbers = sorted(int(e.get('season_number', 0)) for e in show.get('seasons') or [])
        numbers = [n for n in numbers if n > 0] + [n for n in numbers if n == 0]
        label = xbmc.getLocalizedString(20366)
    else:
        numbers = [int(params['s'])]
    items = []
    cached = model.meta_many(meta.key_season(show_id, n) for n in numbers)
    for number in numbers:
        data = cached[meta.key_season(show_id, number)]
        if 'episodes' not in data:
            data = tmdb.season(show_id, number) or {}
        model._cache[meta.key_season(show_id, number)] = data
        if not everything:
            label = _text(STRING_SPECIALS) if number == 0 else (data.get('name') or '{} {}'.format(_text(STRING_SEASON), number))
        for episode in sorted(data.get('episodes') or [], key=lambda e: e.get('episode_number', 0)):
            item = episode_item(show_id, show, number, episode, states.get((number, int(episode.get('episode_number', 0)))),
                                today, everything)
            items.append((item.getPath(), item, False))
    menu.attach_items(items, model)
    _finish(handle, items, 'episodes', [xbmcplugin.SORT_METHOD_UNSORTED],
            'Moje seriály / {} / {}'.format(show.get('name', ''), label))


# --------------------------------------------------------------------------- entry points

def run(handle, params):
    started = time.perf_counter()
    kind = params.get('kind', '')
    try:
        if kind == 'movies':
            movies(handle, params.get('genre'), params.get('skin') == '1')
        elif kind == 'series':
            series(handle, params.get('genre'), params.get('skin') == '1')
        elif kind == 'seasons':
            seasons(handle, params)
        elif kind == 'episodes':
            episodes(handle, params)
        else:
            _finish(handle, [], 'files', [])
    except Exception as exc:                  # a broken listing must close its directory (Kodi waits for it)
        xbmc.log('[Milionář Library] listing {} failed: {}: {}'.format(kind, type(exc).__name__, exc), xbmc.LOGERROR)
        xbmcplugin.endOfDirectory(handle, succeeded=False)
        return
    xbmc.log('[Milionář Library] mlist {} in {:.0f} ms'.format(kind, (time.perf_counter() - started) * 1000), xbmc.LOGDEBUG)


def upcoming(handle, params):
    """Click on an unaired episode: resolve to nothing and tell when it airs."""
    if handle >= 0:                                   # started by RunPlugin (the skin's onclick): no handle to resolve
        xbmcplugin.setResolvedUrl(handle, False, xbmcgui.ListItem(offscreen=True))
    xbmcgui.Dialog().notification('Milionář', params.get('text', ''), xbmcgui.NOTIFICATION_INFO, 5000)
