Skip to content

Commit

Permalink
Version 2.1.0
Browse files Browse the repository at this point in the history
New features¹:
* Search (#106, will not work on some devices)
* Manual playlist reload (#99)
* Manual program guide reload
* Preferences on other devices than Plex Web (will not work on some
devices)
* Use group or stream title as a .png file filename (#108)
Other changes:
* Some random streams added to sample playlist from FreetuxTV DB
* Minor fixes

¹All new features are disabled by default and must be enabled in
Preferences
  • Loading branch information
Cigaras committed Apr 18, 2017
1 parent e74160a commit aafdb5f
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 54 deletions.
121 changes: 96 additions & 25 deletions Contents/Code/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# Version 2.0.5
# Version 2.1.0

from m3u_parser import LoadPlaylist, PlaylistReloader
from xmltv_parser import GuideReloader
from xmltv_parser import LoadGuide, GuideReloader
import re

NAME = 'IPTV'
PREFIX = '/video/iptv'
PREFIX = '/video/' + NAME.lower()

####################################################################################################
def Start():
Expand All @@ -28,6 +28,8 @@ def Start():
ObjectContainer.art = R('art-default.jpg')
DirectoryObject.thumb = R('icon-folder.png')
DirectoryObject.art = R('art-default.jpg')
InputDirectoryObject.thumb = R('icon-search.png')
InputDirectoryObject.art = R('art-default.jpg')
VideoClipObject.thumb = R('icon-tv.png')
VideoClipObject.art = R('art-default.jpg')

Expand All @@ -41,36 +43,89 @@ def Start():
@handler(PREFIX, NAME)
def MainMenu():

if Prefs['search'] or Prefs['m3u_manual_reload'] or Prefs['xmltv_manual_reload'] or Prefs['preferences']:
oc = ObjectContainer()
oc.add(
DirectoryObject(
key = Callback(ListGroups),
title = unicode(L('View playlist')),
thumb = R('icon-list.png')
)
)
if Prefs['search']:
oc.add(
InputDirectoryObject(
key = Callback(ListItems),
title = unicode(L('Search')),
#prompt = unicode(L('Search')),
thumb = R('icon-search.png')
)
)
if Prefs['m3u_manual_reload']:
oc.add(
DirectoryObject(
key = Callback(LoadPlaylist),
title = unicode(L('Reload playlist')),
thumb = R('icon-reload.png')
)
)
if Prefs['xmltv'] and Prefs['xmltv_manual_reload']:
oc.add(
DirectoryObject(
key = Callback(LoadGuide),
title = unicode(L('Reload program guide')),
thumb = R('icon-reload.png')
)
)
if Prefs['preferences']:
oc.add(
PrefsObject(
title = unicode(L('Preferences')),
thumb = R('icon-prefs.png')
)
)
return oc
else:
return ListGroups()

####################################################################################################
@route(PREFIX + '/listgroups', page = int)
def ListGroups(page = 1):

if not Dict['groups']:
LoadPlaylist()
if not Dict['groups']:
return ObjectContainer(header = unicode(L("Error")), message = unicode(L("Provided playlist files are invalid, missing or empty, check the log file for more information")))
return ObjectContainer(
title1 = unicode(L('Error')),
header = unicode(L('Error')),
message = unicode(L('Provided playlist files are invalid, missing or empty, check the log file for more information'))
)

groups = Dict['groups']
groups_list = groups.values()

use_groups = False
for group in groups_list:
if group['title'] not in [unicode(L('All')), unicode(L('No Category'))]:
if group['title'] not in [unicode(L('All')), unicode(L('No category'))]:
use_groups = True
break

if use_groups:
if Prefs['sort_groups']:
# Natural sort (http://stackoverflow.com/a/16090640)
groups_list.sort(key = lambda dict: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', dict['title'].lower())])
groups_list.sort(key = lambda d: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', d['title'].lower())])
else:
groups_list.sort(key = lambda dict: dict['order'])
oc = ObjectContainer()
groups_list.sort(key = lambda d: d['order'])
oc = ObjectContainer(title1 = unicode(L('View playlist')))
oc.add(
DirectoryObject(
key = Callback(ListItems, group = unicode(L('All'))),
title = unicode(L('All'))
)
)
for group in groups_list:
if group['title'] not in [unicode(L('All')), unicode(L('No Category'))]:
thumb = GetImage(group['thumb'], default = 'icon-folder.png')
if group['title'] not in [unicode(L('All')), unicode(L('No category'))]:
thumb = GetImage(group['thumb'], default = 'icon-folder.png', title = group['title'])
art = GetImage(group['art'], default = 'art-default.png')
oc.add(
DirectoryObject(
Expand All @@ -80,37 +135,45 @@ def MainMenu():
art = art
)
)
if unicode(L('No Category')) in groups.keys():
if unicode(L('No category')) in groups.keys():
oc.add(
DirectoryObject(
key = Callback(ListItems, group = unicode(L('No Category'))),
title = unicode(L('No Category'))
key = Callback(ListItems, group = unicode(L('No category'))),
title = unicode(L('No category'))
)
)
return oc
else:
return ListItems(unicode(L('All')))
return ListItems()

####################################################################################################
@route(PREFIX + '/listitems', page = int)
def ListItems(group, page = 1):
def ListItems(group = unicode(L('All')), query = '', page = 1):

if not Dict['streams']:
LoadPlaylist()
if not Dict['streams']:
return ObjectContainer(header = unicode(L("Error")), message = unicode(L("Provided playlist files are invalid, missing or empty, check the log file for more information")))
return ObjectContainer(
title1 = unicode(L('Error')),
header = unicode(L('Error')),
message = unicode(L('Provided playlist files are invalid, missing or empty, check the log file for more information'))
)

group = unicode(group) # Plex loses unicode formating when passing string between @route procedures if string is not a part of a @route

streams = Dict['streams']
items_list = streams.get(group, dict()).values()

# Filter
if query:
items_list = filter(lambda d: query.lower() in d['title'].lower(), items_list)

# Sort
if Prefs['sort_lists']:
# Natural sort (http://stackoverflow.com/a/16090640)
items_list.sort(key = lambda dict: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', dict['title'].lower())])
items_list.sort(key = lambda d: [int(t) if t.isdigit() else t.lower() for t in re.split('(\d+)', d['title'].lower())])
else:
items_list.sort(key = lambda dict: dict['order'])
items_list.sort(key = lambda d: d['order'])

# Number of items per page
try:
Expand All @@ -119,16 +182,16 @@ def ListItems(group, page = 1):
items_per_page = 40
items_list_range = items_list[page * items_per_page - items_per_page : page * items_per_page]

oc = ObjectContainer(title1 = group)
oc = ObjectContainer(title1 = unicode(L('Search')) if query else group)

for item in items_list_range:
oc.add(
CreateVideoClipObject(
url = item['url'],
title = item['title'],
thumb = GetImage(item['thumb'], 'icon-tv.png'),
art = GetImage(item['art'], 'art-default.jpg'),
summary = GetSummary(item['id'], item['name'], item['title'], unicode(L("No description available"))),
thumb = GetImage(item['thumb'], default = 'icon-tv.png', title = item['title']),
art = GetImage(item['art'], default = 'art-default.jpg'),
summary = GetSummary(item['id'], item['name'], item['title'], unicode(L('No description available'))),
c_audio_codec = item['audio_codec'] if item['audio_codec'] else None,
c_video_codec = item['video_codec'] if item['video_codec'] else None,
c_container = item['container'] if item['container'] else None,
Expand All @@ -141,15 +204,19 @@ def ListItems(group, page = 1):
if len(items_list) > page * items_per_page:
oc.add(
NextPageObject(
key = Callback(ListItems, group = group, page = page + 1),
key = Callback(ListItems, group = group, query = query, page = page + 1),
thumb = R('icon-next.png')
)
)

if len(oc) > 0:
return oc
else:
return ObjectContainer(header = "Empty", message = "There are no more items available")
return ObjectContainer(
header = unicode(L('Search')),
header = unicode(L('Search')),
message = unicode(L('No items were found'))
)

####################################################################################################
@route(PREFIX + '/createvideoclipobject', include_container = bool)
Expand Down Expand Up @@ -204,7 +271,10 @@ def PlayVideo(url):
return IndirectResponse(VideoClipObject, key = url)

####################################################################################################
def GetImage(file_name, default):
def GetImage(file_name, default, title = ''):

if Prefs['title_filename'] and not file_name and title:
file_name = title + '.png'

if file_name:
if file_name.startswith('http'):
Expand All @@ -223,6 +293,7 @@ def GetImage(file_name, default):
r = R(file_name)
if r:
return r

return R(default)

####################################################################################################
Expand Down
27 changes: 23 additions & 4 deletions Contents/Code/m3u_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@
####################################################################################################
def LoadPlaylist():

if Dict['playlist_loading_in_progress']:
return ObjectContainer(header = unicode(L('Warning')), message = unicode(L('Playlist is reloading in the background, please wait')))

Dict['playlist_loading_in_progress'] = True

groups = {}
streams = {}
m3u_files = Prefs['playlist'].split(';')
Expand All @@ -30,6 +35,20 @@ def LoadPlaylist():
Dict['last_playlist_load_datetime'] = Datetime.Now()
Dict['last_playlist_load_prefs'] = Prefs['playlist']
Dict['last_playlist_load_filename_groups'] = Prefs['filename_groups']
Dict['playlist_loading_in_progress'] = False

if Dict['groups']:
return ObjectContainer(
title1 = unicode(L('Success')),
header = unicode(L('Success')),
message = unicode(L('Playlist reloaded successfully'))
)
else:
return ObjectContainer(
title1 = unicode(L('Error')),
header = unicode(L('Error')),
message = unicode(L('Provided playlist files are invalid, missing or empty, check the log file for more information'))
)

####################################################################################################
def LoadM3UFile(m3u_file, groups = {}, streams = {}, cust_m3u_name = None):
Expand Down Expand Up @@ -104,7 +123,7 @@ def LoadM3UFile(m3u_file, groups = {}, streams = {}, cust_m3u_name = None):
if not any(item['url'] == stream['url'] for item in streams[unicode(L('All'))].values()):
streams.setdefault(unicode(L('All')), {})[stream_count] = stream
if not group_title:
group_title = unicode(L('No Category') if not m3u_name else m3u_name)
group_title = unicode(L('No category') if not m3u_name else m3u_name)
if group_title not in groups.keys():
group_thumb = GetAttribute(line_1, 'group-logo')
group_art = GetAttribute(line_1, 'group-art')
Expand Down Expand Up @@ -146,11 +165,11 @@ def DecodeURIComponent(uri):
return uri.decode('utf8')

####################################################################################################
def GetAttribute(text, attribute, delimiter1 = '="', delimiter2 = '"', default = ''):
def GetAttribute(text, attribute, delimiter1 = '=', delimiter2 = '"', default = ''):

x = text.lower().find(attribute.lower())
x = text.lower().find(attribute.lower() + delimiter1 + delimiter2)
if x > -1:
y = text.lower().find(delimiter1.lower(), x + len(attribute)) + len(delimiter1)
y = x + len(attribute) + len(delimiter1) + len(delimiter2)
z = text.lower().find(delimiter2.lower(), y) if delimiter2 else len(text)
if z == -1:
z = len(text)
Expand Down
21 changes: 20 additions & 1 deletion Contents/Code/xmltv_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@
####################################################################################################
def LoadGuide():

if Dict['guide_loading_in_progress']:
return ObjectContainer(header = unicode(L('Warning')), message = unicode(L('Program guide is reloading in the background, please wait')))

Dict['guide_loading_in_progress'] = True

channels = {}
guide = {}
xmltv_files = Prefs['xmltv'].split(';')

for xmltv_file in xmltv_files:
if xmltv_file:
if xmltv_file.startswith('http://') or xmltv_file.startswith('https://'):
# Plex can't handle compressed files, using standart Python methods instead
# Plex can't handle compressed files, using standard Python methods instead
if xmltv_file.endswith('.gz') or xmltv_file.endswith('.gz?raw=1'):
f = io.BytesIO(urllib2.urlopen(xmltv_file).read())
try:
Expand Down Expand Up @@ -84,6 +89,20 @@ def LoadGuide():
Dict['guide'] = guide
Dict['last_guide_load_datetime'] = Datetime.Now()
Dict['last_guide_load_prefs'] = Prefs['xmltv']
Dict['guide_loading_in_progress'] = False

if Dict['guide']:
return ObjectContainer(
title1 = unicode(L('Success')),
header = unicode(L('Success')),
message = unicode(L('Program guide reloaded successfully'))
)
else:
return ObjectContainer(
title1 = unicode(L('Error')),
header = unicode(L('Error')),
message = unicode(L('Provided program guide files are invalid, missing or empty, check the log file for more information'))
)

####################################################################################################
def StringToLocalDatetime(arg_string):
Expand Down
Loading

0 comments on commit aafdb5f

Please sign in to comment.