Repository navigation
Host standard features
A host that only lists videos still works, but users expect more today: the watched flag, favourites, the "downloaded" marker, clean file names, a page jump, the INFO key. All of this is provided by E2iPlayer itself - the host only has to give its rows the right fields and call a few helpers.
Every snippet on this page comes from the template 🔗hostTemplate.py, which uses all of them together. Copy it when you start a new host.
- 1. Python 2 and 3
- 2. Rows and stable urls
- 3. Watched flag
- 4. Favourites
- 5. Downloaded marker
- 6. Name normalisation
- 7. Sidecar files
- 8. INFO key and movie metadata
- 9. First page, Jump, Next page
- 10. Messages instead of empty lists
- Checklist
E2iPlayer exists in two forks: this one (Python 3 images) and e2iplayer-zadmario, which also runs on Python 2.7 images. Hosts move between the two, so write them for both:
- no f-strings - use
'%s' % xor'{}'.format(x) - no
print- useprintDBG()/printExc()fromtools/iptvtools.py -
urllib/urlparseonly through the p2p3 shims, which give the same names on both versions:from Plugins.Extensions.IPTVPlayer.p2p3.UrlLib import urllib_quote, urllib_quote_plus, urllib_unquote, urllib_urlencode from Plugins.Extensions.IPTVPlayer.p2p3.UrlParse import urljoin, urlparse, parse_qs from Plugins.Extensions.IPTVPlayer.p2p3.manipulateStrings import ensure_str, ensure_binary
- JSON through
libs/e2ijson.py(from Plugins.Extensions.IPTVPlayer.libs.e2ijson import loads as json_loads, dumps as json_dumps) - HTTP only through
self.cm(getPage,getPageCFProtection,saveWebFile) - norequests, noos.system/subprocess, notime.sleep - check both before a pull request:
python2 -m py_compile hostname.pyandpython3 -m py_compile hostname.py
Newer framework parts such as components/configsecret.py (hidden passwords) and tools/iptvpaging.py (see 9) are in both forks now (python3 and zadmario). The template still imports iptvpaging with a fallback, so it also loads on older installations without the module.
Every row of a list is a plain dict, added with self.addDir() (folder), self.addVideo(), self.addAudio(), self.addMarker() ... The keys E2iPlayer reads:
| key | meaning |
|---|---|
title |
shown in the list; for a video also the download file name |
url |
the url of the row - for videos the page of the movie/episode, not the stream |
icon |
cover in the main window |
desc |
text below the list |
good_for_fav |
True: MENU offers "Add item to favorites" |
category |
your own routing key for handleService()
|
page, last_page
|
paging, see 9 |
Any other key is yours and comes back in cItem when the row is opened.
Watched flag, favourites and the downloaded marker all recognise a row again by its url and its fields. So:
- the
urlof a movie/episode row must be the same on every visit - the page url or an id, never a signed or expiring stream url - build a row as a new dict, not as
dict(cItem)of the list it is in - otherwisepage, search terms and other list fields stick to every row (and to every favourite made from it):
params = {'name': 'category', 'good_for_fav': True, 'url': itemUrl, 'icon': icon, 'desc': desc,
's_title': title, 'meta_type': 'movie', 'meta_title': title, 'meta_year': year}
params.update({'category': 'video', 'title': title})
self.addVideo(params)dict(cItem) is fine one level down, e.g. for the seasons of a series - there the row should inherit the series fields.
What the user gets: a started/watched badge on videos (set when playback starts and after 95 %), "Set watched"/"Unset watched" in the MENU, and folders (season, series) that show watched when all their episodes are. Switch: Settings → Service configuration → Allow watched flag to be set.
The host needs three things.
a) The mixins and the helper (tools/iptvwatchedfoldermixin.py, tools/iptvwatchedhelper.py):
from Plugins.Extensions.IPTVPlayer.tools.iptvwatchedhelper import IPTVWatchedHelper
from Plugins.Extensions.IPTVPlayer.tools.iptvwatchedfoldermixin import GenericFolderWatchedScraperMixin, GenericFolderWatchedHostMixin
class MyHost(GenericFolderWatchedScraperMixin, CBaseHostClass): # mixin BEFORE CBaseHostClass
def __init__(self):
CBaseHostClass.__init__(self, {'history': 'myhost', 'cookie': 'myhost.cookie'})
...
self.watchedHelper = IPTVWatchedHelper('myhost')
self.wfInitFolderCache()
class IPTVHost(GenericFolderWatchedHostMixin, CHostBase): # mixin BEFORE CHostBase
def __init__(self):
CHostBase.__init__(self, MyHost(), True, [])
self.cachedRet = None
self.refreshAfterWatchedFlagChange = False
self.watchedHelper = IPTVWatchedHelper('myhost')b) A key per row - _getWatchedKeyForItem() returns a stable id, or '' for rows without a flag (menus, "Next page", search, live streams):
def _getWatchedKeyForItem(self, cItem):
try:
if not isinstance(cItem, dict):
return ''
url = str(cItem.get('url', '') or '').strip()
category = cItem.get('category', '')
if url == '':
return ''
if cItem.get('type', '') in ('video', 'audio'):
return 'video:%s' % url
if category == 'list_seasons':
return 'folder:%s' % url
if category == 'list_episodes':
return 'folder:%s|%s' % (url, cItem.get('season', ''))
except Exception:
printExc()
return ''c) Nothing else. The scraper mixin replaces addDir()/addVideo() and remembers which folder a row was listed in, so a finished episode marks its season and series. Because of that, a host with the mixin must not override addDir()/addVideo() itself.
Keep the key independent of paging: page 2 of a season is the same folder as page 1. For urls with paging parameters, self.wfNormalizeUrlKey(url) removes page, offset, limit and similar query parameters.
What the user gets: MENU → "Add item to favorites" on every row with good_for_fav: True, a star at the end of rows that are already favourites (Settings → Service configuration → Mark favourite items), and the favourite opens again from the Favourites host.
By default the favourite stores the whole row (json_dumps(cItem)). That breaks when the same title comes with a different year, quality or plot from another list - it is not recognised as the same favourite. So store only what identifies the row and is needed to open it again:
FAV_FIELDS = ('name', 'type', 'category', 'url', 's_title', 'season', 'meta_type', 'meta_title', 'meta_year')
def getFavouriteData(self, cItem):
try:
return json_dumps(dict((key, cItem[key]) for key in self.FAV_FIELDS if key in cItem))
except Exception:
printExc()
return CBaseHostClass.getFavouriteData(self, cItem)Opening a favourite calls your normal code with this dict as cItem:
- a favourite video →
getLinksForVideo(cItem) - a favourite folder →
handleService()with the dict as the selected row, solistSeasons(cItem)etc. must work with only these fields - usecItem.get(...), not values from earlier lists
Rows that only page or search ("Next page", "Jump", search history) get good_for_fav: False.
What the user gets: a marker at the end of a row while it downloads and after the download finished (Settings → Service configuration → Mark downloaded items). It is also shown inside the favourites.
There is nothing to call - tools/iptvdownloaded.py keys the row by host name + the row's url (its title when it has no url). It works when:
- movies and episodes are VIDEO rows (
addVideo) with a stable pageurl(see 2) - the links are fetched in
getLinksForVideo()when the row is opened, not stored in the row
Live rows: give a live stream row 'live': True (or 'is_live': True) in its dict. Such a row can't be marked in the selection mode, is never taken by a batch download, and a recording of it (GREEN) gets no downloaded marker. A link with iptv_livestream in its meta also gets no marker.
Batch download (Download all items of this page / Download marked items) reopens a row later the way a favourite is opened: with getFavouriteData() of the row (see 4) and getLinksForFavourite(). So a row that works as a favourite also works in a batch download - nothing else to do. See Playlists and selection.
The title of a video row becomes the file name of its download, so it should be clean: no colour codes, no "HD | 1080p | German" tails. With Settings → Downloading configuration → Normalize item / file names (Show - SxxExx - Title) switched on (default), hosts use this form:
- movies:
Title (Year) - episodes:
Show - S01E02 - Episode name
With the option off, use the site's own label. The helpers are in tools/iptvnaming.py (formatSxxExx, parseSxxExx, extractNum, normalizeMediathekTitle for the mediathek-style hosts):
from Plugins.Extensions.IPTVPlayer.components.iptvconfigmenu import IsMediaNamingNormalized
from Plugins.Extensions.IPTVPlayer.tools.iptvnaming import formatSxxExx
normalize = IsMediaNamingNormalized()
...
# movie
title = '%s (%s)' % (title, year) if (normalize and year) else title
# episode
if normalize and epNum:
title = ' - '.join([x for x in (sTitle, formatSxxExx(season, epNum), epName) if x])
else:
title = '%s - %s' % (sTitle, ('%s %s' % (numLabel, epName)).strip())Keep the plain title in a field of your own (s_title in the template) - it is needed for the series name of the episodes and for the metadata lookup.
What the user gets: next to a downloaded Movie (2020).mp4 a Movie (2020).txt with the plot and a Movie (2020).jpg with the poster (Settings → Downloading configuration → Create sidecar files (.txt/.jpg)), for media servers and other players.
The host puts plot and poster into the url meta of its links, the download manager writes the files (libs/urlmetahelper.py). Two places:
from Plugins.Extensions.IPTVPlayer.components.iptvconfigmenu import IsSidecarEnabled
from Plugins.Extensions.IPTVPlayer.libs.urlmetahelper import buildSidecarFromItem, applySidecarToLinks, sidecarFromUrlMeta, decorateResolvedLinkItems
def getLinksForVideo(self, cItem):
...
# plot = a longer text from the movie page, if there is one; the row's desc and icon are added
return applySidecarToLinks(linksTab, buildSidecarFromItem(cItem, IsSidecarEnabled(), plot))
def getVideoLinks(self, videoUrl):
# the links need_resolve=1 come back here: carry the sidecar over to the resolved streams
if not self.cm.isValidUrl(videoUrl):
return []
sidecar = sidecarFromUrlMeta(videoUrl, IsSidecarEnabled())
return decorateResolvedLinkItems(self.up.getVideoLinkExt(videoUrl), sidecar)What the user gets: INFO on a movie or series shows plot, poster, rating, genre, cast, runtime - from the services switched on in Settings → Metadata providers configuration (TMDb and OMDb with the user's own free API key, IMDb, TVmaze and Cinemeta without key). Texts come in the user's language where the service has them. Results are cached.
libs/moviemeta.py does the lookup. The rows carry three fields:
params.update({'meta_type': 'movie', # or 'tv' for a series and its seasons/episodes
'meta_title': title, # the plain title, without year or quality
'meta_year': year}) # '' when unknownand the host returns its INFO content from it:
from Plugins.Extensions.IPTVPlayer.libs.moviemeta import getArticleContent as getMetaArticleContent
class MyHost(...):
def getArticleContent(self, cItem):
return getMetaArticleContent(cItem) # falls back to the row's desc and icon
class IPTVHost(...):
def withArticleContent(self, cItem):
return 'meta_title' in cItem # which rows have INFOWhen the site gives an IMDb id (tt1234567), getMetaByImdbId('movie', imdbId) finds the title exactly. Both getMeta() and getMetaByImdbId() return {'title', 'plot', 'poster', 'info'} or {}, so a host can also merge the result with its own data. Never put an API key into a host - the keys are the user's own settings.
Not for sport clips, news, radio or music videos - there getArticleContent() returns the site's own data, or the host has no INFO.
What the user gets: "Next page" (with (2/12) when the last page is known), from page 2 on "First Page", and "Jump", which asks for the page number with the numeric keypad. The header shows Page: 2/12.
"Next page" is a folder row whose title is exactly _('Next page'). Put the page it opens into page, and the last page into last_page when the site shows it:
params = dict(cItem)
params.update({'good_for_fav': False, 'title': _('Next page'), 'page': page + 1, 'last_page': lastPage})
self.addDir(params)Only add it when the site really has a next page - a "Next page" that opens an empty list is a bug.
First page + Jump + Next page together come from tools/iptvpaging.py. The host keeps the url of its list as a template with {page} in it:
from Plugins.Extensions.IPTVPlayer.tools.iptvpaging import addPagingItems, isJumpItem, jumpTarget
MENU = [{'category': 'list_items', 'title': _('Movies'), 'url_tpl': self.getFullUrl('/movies/page/{page}/')}]
def listItems(self, cItem):
page = int(cItem.get('page', 1) or 1)
sts, data = self.getPage(cItem['url_tpl'].format(page=page))
... # the rows of this page
addPagingItems(self, cItem, page, hasNext, lastPage, cItem.get('url_tpl', ''))
def handleService(self, index, refresh=0, searchPattern='', searchType=''):
CBaseHostClass.handleService(self, index, refresh, searchPattern, searchType)
if isJumpItem(self.currItem):
# asks for the page number and turns the row into the list item of that page
self.currItem = jumpTarget(self, self.currItem)
name = self.currItem.get('name', '')
category = self.currItem.get('category', '')
...addPagingItems(host, cItem, page, hasNext, lastPage=0, pageUrlTpl='', nextParams=None): page is the page just listed (1-based), lastPage 0 when unknown. Without pageUrlTpl there is no "Jump", and the list function has to build the url from cItem['page'] itself. nextParams adds fields to the "Next page" row only (e.g. a cursor the site's API returns).
If a host builds its rows with dict(cItem) anyway, remove the paging fields from them with stripPagerKeys(params) from the same module - otherwise a folder opened from page 5 starts at page 5.
An empty list tells the user nothing. When a title has no playable link, the site answers with an error, the content is geo-blocked or premium-only, say so:
from Plugins.Extensions.IPTVPlayer.components.iptvplayerinit import SetIPTVPlayerLastHostError
if not linksTab:
SetIPTVPlayerLastHostError(_('This video is only on hosters E2iPlayer cannot play.'))
return []E2iPlayer shows the message as "Last error" below its own "No valid links available." / "No item to display." when the links or the list come back empty - so the message should say why, not repeat that nothing was found. For a hint during listing without stopping, GetIPTVNotify().push(message, 'info', 5) shows a short notification.
New texts are written in English inside _(). Translations are done separately - do not edit .po files in a host pull request.
Before you open a pull request:
- compiles with Python 2.7 and 3 (
py_compile), no f-strings, imports through p2p3 - HTTP only through
self.cm, one User-Agent for the whole host, notime.sleep/os.system - movies/episodes are VIDEO rows with a stable page
url; links are fetched ingetLinksForVideo() - watched key per movie/episode/season/series,
''for menus and paging rows -
good_for_favon titles,getFavouriteData()with a small field set; a favourite opens again - titles:
Title (Year)/Show - SxxExx - Namewhen normalisation is on, no colour codes in video titles - sidecar in
getLinksForVideo()andgetVideoLinks() -
meta_type/meta_title/meta_year+getArticleContent()+withArticleContent()for movies and series - "Next page" only when there is one;
last_pagewhen known; First/Jump viaiptvpagingwhen available - search where the site has one (see Building a search function)
- a message instead of an empty list when nothing can be played
- new texts in English inside
_(), no API keys or passwords in the code - line 2 of the file
# Last Modified: dd.mm.yyyy, updated with every change;gettytul()returns a fixed string - logo and PlayerSelector icons, entry in
hostgroups.txt(see Adding a new host) - tested on a box with a debug log (see How to create debug logs)
- pull request against
python3following CONTRIBUTING.md - including whether AI helped
Using E2iPlayer
- Install
- Controls and navigation
- Settings explained
- Playback and subtitles
- Playlists and selection
- Download manager
- Web interface
- Torrents with TorrServer
Captchas
Problems?
For developers