From 16df4d6404e5bec8a9d9a682c673cae657d51b3d Mon Sep 17 00:00:00 2001
From: angelblue05 <tamara.angel05@gmail.com>
Date: Sat, 5 Nov 2016 13:36:46 -0500
Subject: [PATCH] New views.py

Combines maintainsViews from librarysync and videonodes and playlist
creation. Still need to move deletion from utils.
---
 resources/lib/kodidb_functions.py | 122 -------
 resources/lib/librarysync.py      | 224 +------------
 resources/lib/player.py           |   1 -
 resources/lib/read_embyserver.py  |  16 +-
 resources/lib/service_entry.py    |   4 +-
 resources/lib/videonodes.py       | 392 ----------------------
 resources/lib/views.py            | 519 +++++++++++++++---------------
 7 files changed, 291 insertions(+), 987 deletions(-)
 delete mode 100644 resources/lib/kodidb_functions.py
 delete mode 100644 resources/lib/videonodes.py

diff --git a/resources/lib/kodidb_functions.py b/resources/lib/kodidb_functions.py
deleted file mode 100644
index e486250b..00000000
--- a/resources/lib/kodidb_functions.py
+++ /dev/null
@@ -1,122 +0,0 @@
-# -*- coding: utf-8 -*-
-
-##################################################################################################
-
-import logging
-
-import xbmc
-
-import api
-import artwork
-
-#################################################################################################
-
-log = logging.getLogger("EMBY."+__name__)
-
-#################################################################################################
-
-class Kodidb_Functions(object):
-
-    kodiversion = int(xbmc.getInfoLabel("System.BuildVersion")[:2])
-    
-
-    def __init__(self, cursor):
-
-        self.cursor = cursor
-        self.artwork = artwork.Artwork()
-
-    def createTag(self, name):
-        # This will create and return the tag_id
-        if self.kodiversion in (15, 16, 17):
-            # Kodi Isengard, Jarvis, Krypton
-            query = ' '.join((
-
-                "SELECT tag_id",
-                "FROM tag",
-                "WHERE name = ?",
-                "COLLATE NOCASE"
-            ))
-            self.cursor.execute(query, (name,))
-            try:
-                tag_id = self.cursor.fetchone()[0]
-
-            except TypeError:
-                self.cursor.execute("select coalesce(max(tag_id),0) from tag")
-                tag_id = self.cursor.fetchone()[0] + 1
-
-                query = "INSERT INTO tag(tag_id, name) values(?, ?)"
-                self.cursor.execute(query, (tag_id, name))
-                log.debug("Create tag_id: %s name: %s", tag_id, name)
-        else:
-            # Kodi Helix
-            query = ' '.join((
-
-                "SELECT idTag",
-                "FROM tag",
-                "WHERE strTag = ?",
-                "COLLATE NOCASE"
-            ))
-            self.cursor.execute(query, (name,))
-            try:
-                tag_id = self.cursor.fetchone()[0]
-
-            except TypeError:
-                self.cursor.execute("select coalesce(max(idTag),0) from tag")
-                tag_id = self.cursor.fetchone()[0] + 1
-
-                query = "INSERT INTO tag(idTag, strTag) values(?, ?)"
-                self.cursor.execute(query, (tag_id, name))
-                log.debug("Create idTag: %s name: %s", tag_id, name)
-
-        return tag_id
-
-    def updateTag(self, oldtag, newtag, kodiid, mediatype):
-        # TODO: Move to video nodes eventually
-        log.debug("Updating: %s with %s for %s: %s", oldtag, newtag, mediatype, kodiid)
-
-        if self.kodiversion in (15, 16, 17):
-            # Kodi Isengard, Jarvis, Krypton
-            try:
-                query = ' '.join((
-
-                    "UPDATE tag_link",
-                    "SET tag_id = ?",
-                    "WHERE media_id = ?",
-                    "AND media_type = ?",
-                    "AND tag_id = ?"
-                ))
-                self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,))
-            except Exception:
-                # The new tag we are going to apply already exists for this item
-                # delete current tag instead
-                query = ' '.join((
-
-                    "DELETE FROM tag_link",
-                    "WHERE media_id = ?",
-                    "AND media_type = ?",
-                    "AND tag_id = ?"
-                ))
-                self.cursor.execute(query, (kodiid, mediatype, oldtag,))
-        else:
-            # Kodi Helix
-            try:
-                query = ' '.join((
-
-                    "UPDATE taglinks",
-                    "SET idTag = ?",
-                    "WHERE idMedia = ?",
-                    "AND media_type = ?",
-                    "AND idTag = ?"
-                ))
-                self.cursor.execute(query, (newtag, kodiid, mediatype, oldtag,))
-            except Exception:
-                # The new tag we are going to apply already exists for this item
-                # delete current tag instead
-                query = ' '.join((
-
-                    "DELETE FROM taglinks",
-                    "WHERE idMedia = ?",
-                    "AND media_type = ?",
-                    "AND idTag = ?"
-                ))
-                self.cursor.execute(query, (kodiid, mediatype, oldtag,))
diff --git a/resources/lib/librarysync.py b/resources/lib/librarysync.py
index 0dabebe9..8d302a97 100644
--- a/resources/lib/librarysync.py
+++ b/resources/lib/librarysync.py
@@ -17,10 +17,9 @@ import clientinfo
 import downloadutils
 import itemtypes
 import embydb_functions as embydb
-import kodidb_functions as kodidb
 import read_embyserver as embyserver
 import userclient
-import videonodes
+import views
 from objects import Movies, MusicVideos, TVShows, Music
 from utils import window, settings, language as lang, should_stop
 from ga_client import GoogleAnalytics
@@ -60,7 +59,6 @@ class LibrarySync(threading.Thread):
         self.doUtils = downloadutils.DownloadUtils().downloadUrl
         self.user = userclient.UserClient()
         self.emby = embyserver.Read_EmbyServer()
-        self.vnodes = videonodes.VideoNodes()
 
         self.kodi_version = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
 
@@ -276,7 +274,9 @@ class LibrarySync(threading.Thread):
                 starttotal = datetime.now()
 
                 # Set views
-                self.maintainViews(cursor_emby, cursor_video)
+                views.Views(cursor_emby, cursor_video).maintain()
+                conn_emby.commit()
+                #self.maintainViews(cursor_emby, cursor_video)
 
                 # Sync video library
                 process = {
@@ -362,222 +362,10 @@ class LibrarySync(threading.Thread):
 
     def refreshViews(self):
         
-        with DatabaseConn('emby') as conn_emby, DatabaseConn('video') as conn_video:
+        with DatabaseConn('emby') as conn_emby, DatabaseConn() as conn_video:
             with closing(conn_emby.cursor()) as cursor_emby, closing(conn_video.cursor()) as cursor_video:
                 # Compare views, assign correct tags to items
-                self.maintainViews(cursor_emby, cursor_video)
-
-
-    def maintainViews(self, embycursor, kodicursor):
-
-        # Compare the views to emby
-        emby = self.emby
-        emby_db = embydb.Embydb_Functions(embycursor)
-        kodi_db = kodidb.Kodidb_Functions(kodicursor)
-
-        # Get views
-        result = self.doUtils("{server}/emby/Users/{UserId}/Views?format=json")
-        grouped_views = result['Items'] if 'Items' in result else []
-        ordered_views = self.emby.getViews(sortedlist=True)
-        all_views = []
-        sorted_views = []
-        for view in ordered_views:
-            all_views.append(view['name'])
-            if view['type'] == "music":
-                continue
-
-            if view['type'] == "mixed":
-                sorted_views.append(view['name'])
-            sorted_views.append(view['name'])
-        log.info("Sorted views: %s" % sorted_views)
-
-        # total nodes for window properties
-        self.vnodes.clearProperties()
-        totalnodes = len(sorted_views) + 0
-
-        current_views = emby_db.getViews()
-        # Set views for supported media type
-        emby_mediatypes = {
-
-            'movies': "Movie",
-            'tvshows': "Series",
-            'musicvideos': "MusicVideo",
-            'homevideos': "Video",
-            'music': "Audio",
-            'photos': "Photo"
-        }
-        for mediatype in ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos']:
-
-            nodes = [] # Prevent duplicate for nodes of the same type
-            playlists = [] # Prevent duplicate for playlists of the same type
-            # Get media folders from server
-            folders = self.emby.getViews(mediatype, root=True)
-            for folder in folders:
-
-                folderid = folder['id']
-                foldername = folder['name']
-                viewtype = folder['type']
-
-                if foldername not in all_views:
-                    # Media folders are grouped into userview
-                    params = {
-                        'ParentId': folderid,
-                        'Recursive': True,
-                        'Limit': 1,
-                        'IncludeItemTypes': emby_mediatypes[mediatype]
-                    } # Get one item from server using the folderid
-                    url = "{server}/emby/Users/{UserId}/Items?format=json"
-                    result = self.doUtils(url, parameters=params)
-                    try:
-                        verifyitem = result['Items'][0]['Id']
-                    except (TypeError, IndexError):
-                        # Something is wrong. Keep the same folder name.
-                        # Could be the view is empty or the connection
-                        pass
-                    else:
-                        for grouped_view in grouped_views:
-                            # This is only reserved for the detection of grouped views
-                            if (grouped_view['Type'] == "UserView" and
-                                grouped_view.get('CollectionType') == mediatype):
-                                # Take the userview, and validate the item belong to the view
-                                if self.emby.verifyView(grouped_view['Id'], verifyitem):
-                                    # Take the name of the userview
-                                    log.info("Found corresponding view: %s %s"
-                                        % (grouped_view['Name'], grouped_view['Id']))
-                                    foldername = grouped_view['Name']
-                                    break
-                        else:
-                            # Unable to find a match, add the name to our sorted_view list
-                            sorted_views.append(foldername)
-                            log.info("Couldn't find corresponding grouped view: %s" % sorted_views)
-
-                # Failsafe
-                try:
-                    sorted_views.index(foldername)
-                except ValueError:
-                    sorted_views.append(foldername)
-
-                # Get current media folders from emby database
-                view = emby_db.getView_byId(folderid)
-                try:
-                    current_viewname = view[0]
-                    current_viewtype = view[1]
-                    current_tagid = view[2]
-
-                except TypeError:
-                    log.info("Creating viewid: %s in Emby database." % folderid)
-                    tagid = kodi_db.createTag(foldername)
-                    # Create playlist for the video library
-                    if (foldername not in playlists and
-                            mediatype in ('movies', 'tvshows', 'musicvideos')):
-                        utils.playlistXSP(mediatype, foldername, folderid, viewtype)
-                        playlists.append(foldername)
-                    # Create the video node
-                    if foldername not in nodes and mediatype not in ("musicvideos", "music"):
-                        self.vnodes.viewNode(sorted_views.index(foldername), foldername, mediatype,
-                            viewtype, folderid)
-                        if viewtype == "mixed": # Change the value
-                            sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
-                        nodes.append(foldername)
-                        totalnodes += 1
-                    # Add view to emby database
-                    emby_db.addView(folderid, foldername, viewtype, tagid)
-
-                else:
-                    log.debug(' '.join((
-
-                        "Found viewid: %s" % folderid,
-                        "viewname: %s" % current_viewname,
-                        "viewtype: %s" % current_viewtype,
-                        "tagid: %s" % current_tagid)))
-
-                    # View is still valid
-                    try:
-                        current_views.remove(folderid)
-                    except ValueError:
-                        # View was just created, nothing to remove
-                        pass
-
-                    # View was modified, update with latest info
-                    if current_viewname != foldername:
-                        log.info("viewid: %s new viewname: %s" % (folderid, foldername))
-                        tagid = kodi_db.createTag(foldername)
-
-                        # Update view with new info
-                        emby_db.updateView(foldername, tagid, folderid)
-
-                        if mediatype != "music":
-                            if emby_db.getView_byName(current_viewname) is None:
-                                # The tag could be a combined view. Ensure there's no other tags
-                                # with the same name before deleting playlist.
-                                utils.playlistXSP(
-                                    mediatype, current_viewname, folderid, current_viewtype, True)
-                                # Delete video node
-                                if mediatype != "musicvideos":
-                                    self.vnodes.viewNode(
-                                        indexnumber=None,
-                                        tagname=current_viewname,
-                                        mediatype=mediatype,
-                                        viewtype=current_viewtype,
-                                        viewid=folderid,
-                                        delete=True)
-                            # Added new playlist
-                            if (foldername not in playlists and
-                                    mediatype in ('movies', 'tvshows', 'musicvideos')):
-                                utils.playlistXSP(mediatype, foldername, folderid, viewtype)
-                                playlists.append(foldername)
-                            # Add new video node
-                            if foldername not in nodes and mediatype != "musicvideos":
-                                self.vnodes.viewNode(sorted_views.index(foldername), foldername,
-                                    mediatype, viewtype, folderid)
-                                if viewtype == "mixed": # Change the value
-                                    sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
-                                nodes.append(foldername)
-                                totalnodes += 1
-
-                        # Update items with new tag
-                        items = emby_db.getItem_byView(folderid)
-                        for item in items:
-                            # Remove the "s" from viewtype for tags
-                            kodi_db.updateTag(
-                                current_tagid, tagid, item[0], current_viewtype[:-1])
-                    else:
-                        # Validate the playlist exists or recreate it
-                        if mediatype != "music":
-                            if (foldername not in playlists and
-                                    mediatype in ('movies', 'tvshows', 'musicvideos')):
-                                utils.playlistXSP(mediatype, foldername, folderid, viewtype)
-                                playlists.append(foldername)
-                            # Create the video node if not already exists
-                            if foldername not in nodes and mediatype != "musicvideos":
-                                self.vnodes.viewNode(sorted_views.index(foldername), foldername,
-                                    mediatype, viewtype, folderid)
-                                if viewtype == "mixed": # Change the value
-                                    sorted_views[sorted_views.index(foldername)] = "%ss" % foldername
-                                nodes.append(foldername)
-                                totalnodes += 1
-        else:
-            # Add video nodes listings
-            self.vnodes.singleNode(totalnodes, "Favorite movies", "movies", "favourites")
-            totalnodes += 1
-            self.vnodes.singleNode(totalnodes, "Favorite tvshows", "tvshows", "favourites")
-            totalnodes += 1
-            self.vnodes.singleNode(totalnodes, "Favorite episodes", "episodes", "favourites")
-            totalnodes += 1
-            self.vnodes.singleNode(totalnodes, "channels", "movies", "channels")
-            totalnodes += 1
-            # Save total
-            window('Emby.nodes.total', str(totalnodes))
-
-            # Remove any old referenced views
-            log.info("Removing views: %s" % current_views)
-            for view in current_views:
-                emby_db.removeView(view)
-                # Remove any items that belongs to the old view
-                items = emby_db.get_item_by_view(view)
-                items = [i[0] for i in items] # Convert list of tuple to list
-                self.triage_items("remove", items)
-
+                views.Views(cursor_emby, cursor_video).maintain()
 
     def movies(self, embycursor, kodicursor, pdialog):
 
diff --git a/resources/lib/player.py b/resources/lib/player.py
index 9505238d..9f196e66 100644
--- a/resources/lib/player.py
+++ b/resources/lib/player.py
@@ -11,7 +11,6 @@ import xbmcgui
 
 import clientinfo
 import downloadutils
-import kodidb_functions as kodidb
 import websocket_client as wsc
 from utils import window, settings, language as lang
 from ga_client import GoogleAnalytics, log_error
diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py
index b201e807..3c1718dd 100644
--- a/resources/lib/read_embyserver.py
+++ b/resources/lib/read_embyserver.py
@@ -31,6 +31,9 @@ class Read_EmbyServer():
         self.userId = window('emby_currUser')
         self.server = window('emby_server%s' % self.userId)
 
+    def get_emby_url(self, handler):
+        return "{server}/emby/%s" % handler
+
 
     def split_list(self, itemlist, size):
         # Split up list in pieces of size. Will generate a list of lists
@@ -589,4 +592,15 @@ class Read_EmbyServer():
         data = {'username': username, 'password': hashlib.sha1(password).hexdigest()}
         user = self.doUtils(url, postBody=data, action_type="POST", authenticate=False)
 
-        return user
\ No newline at end of file
+        return user
+
+    def get_single_item(self, media_type, parent_id):
+
+        params = {
+            'ParentId': parent_id,
+            'Recursive': True,
+            'Limit': 1,
+            'IncludeItemTypes': media_type
+        }
+        url = self.get_emby_url('Users/{UserId}/Items?format=json')
+        return self.doUtils(url, parameters=params)
\ No newline at end of file
diff --git a/resources/lib/service_entry.py b/resources/lib/service_entry.py
index f730d8fc..273dce45 100644
--- a/resources/lib/service_entry.py
+++ b/resources/lib/service_entry.py
@@ -16,8 +16,8 @@ import initialsetup
 import kodimonitor
 import librarysync
 import player
-import videonodes
 import websocket_client as wsc
+from views import VideoNodes
 from utils import window, settings, dialog, language as lang
 from ga_client import GoogleAnalytics
 import hashlib
@@ -77,7 +77,7 @@ class Service(object):
             window(prop, clear=True)
 
         # Clear video nodes properties
-        videonodes.VideoNodes().clearProperties()
+        VideoNodes().clearProperties()
 
         # Set the minimum database version
         window('emby_minDBVersion', value="1.1.63")
diff --git a/resources/lib/videonodes.py b/resources/lib/videonodes.py
deleted file mode 100644
index 02f2f17e..00000000
--- a/resources/lib/videonodes.py
+++ /dev/null
@@ -1,392 +0,0 @@
-# -*- coding: utf-8 -*-
-
-#################################################################################################
-
-import logging
-import shutil
-import xml.etree.ElementTree as etree
-
-import xbmc
-import xbmcaddon
-import xbmcvfs
-
-import utils
-from utils import window, language as lang
-
-#################################################################################################
-
-log = logging.getLogger("EMBY."+__name__)
-
-#################################################################################################
-
-
-class VideoNodes(object):
-
-
-    def __init__(self):
-
-        self.kodiversion = int(xbmc.getInfoLabel('System.BuildVersion')[:2])
-
-
-    def commonRoot(self, order, label, tagname, roottype=1):
-
-        if roottype == 0:
-            # Index
-            root = etree.Element('node', attrib={'order': "%s" % order})
-        elif roottype == 1:
-            # Filter
-            root = etree.Element('node', attrib={'order': "%s" % order, 'type': "filter"})
-            etree.SubElement(root, 'match').text = "all"
-            # Add tag rule
-            rule = etree.SubElement(root, 'rule', attrib={'field': "tag", 'operator': "is"})
-            etree.SubElement(rule, 'value').text = tagname
-        else:
-            # Folder
-            root = etree.Element('node', attrib={'order': "%s" % order, 'type': "folder"})
-
-        etree.SubElement(root, 'label').text = label
-        etree.SubElement(root, 'icon').text = "special://home/addons/plugin.video.emby/icon.png"
-
-        return root
-
-    def viewNode(self, indexnumber, tagname, mediatype, viewtype, viewid, delete=False):
-
-        if viewtype == "mixed":
-            dirname = "%s - %s" % (viewid, mediatype)
-        else:
-            dirname = viewid
-        
-        path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
-        nodepath = xbmc.translatePath(
-                    "special://profile/library/video/emby/%s/" % dirname).decode('utf-8')
-
-        # Verify the video directory
-        if not xbmcvfs.exists(path):
-            try:
-                shutil.copytree(
-                    src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
-                    dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
-            except Exception as error:
-                log.error(error)
-
-            xbmcvfs.exists(path)
-
-        if delete:
-            dirs, files = xbmcvfs.listdir(nodepath)
-            for file in files:
-                xbmcvfs.delete(nodepath + file)
-
-            log.info("Sucessfully removed videonode: %s." % tagname)
-            return
-        # Create the node directory
-        if not xbmcvfs.exists(nodepath) and not mediatype == "photos":
-            # We need to copy over the default items
-            xbmcvfs.mkdirs(nodepath)
-
-        # Create index entry
-        nodeXML = "%sindex.xml" % nodepath
-        # Set windows property
-        path = "library://video/emby/%s/" % dirname
-        for i in range(1, indexnumber):
-            # Verify to make sure we don't create duplicates
-            if window('Emby.nodes.%s.index' % i) == path:
-                return
-
-        if mediatype == "photos":
-            path = "plugin://plugin.video.emby/?id=%s&mode=getsubfolders" % indexnumber
-            
-        window('Emby.nodes.%s.index' % indexnumber, value=path)
-        
-        # Root
-        if not mediatype == "photos":
-            if viewtype == "mixed":
-                specialtag = "%s - %s" % (tagname, mediatype)
-                root = self.commonRoot(order=0, label=specialtag, tagname=tagname, roottype=0)
-            else:
-                root = self.commonRoot(order=0, label=tagname, tagname=tagname, roottype=0)
-            try:
-                utils.indent(root)
-            except: pass
-            etree.ElementTree(root).write(nodeXML)
-
-        nodetypes = {
-
-            '1': "all",
-            '2': "recent",
-            '3': "recentepisodes",
-            '4': "inprogress",
-            '5': "inprogressepisodes",
-            '6': "unwatched",
-            '7': "nextepisodes",
-            '8': "sets",
-            '9': "genres",
-            '10': "random",
-            '11': "recommended",
-        }
-        mediatypes = {
-            # label according to nodetype per mediatype
-            'movies': 
-                {
-                '1': tagname,
-                '2': 30174,
-                '4': 30177,
-                '6': 30189,
-                '8': 20434,
-                '9': 135,
-                '10': 30229,
-                '11': 30230
-                },
-
-            'tvshows': 
-                {
-                '1': tagname,
-                '2': 30170,
-                '3': 30175,
-                '4': 30171,
-                '5': 30178,
-                '7': 30179,
-                '9': 135,
-                '10': 30229,
-                '11': 30230
-                },
-                
-            'homevideos': 
-                {
-                '1': tagname,
-                '2': 30251,
-                '11': 30253
-                },
-                
-            'photos': 
-                {
-                '1': tagname,
-                '2': 30252,
-                '8': 30255,
-                '11': 30254
-                },
-
-            'musicvideos': 
-                {
-                '1': tagname,
-                '2': 30256,
-                '4': 30257,
-                '6': 30258
-                }
-        }
-
-        nodes = mediatypes[mediatype]
-        for node in nodes:
-
-            nodetype = nodetypes[node]
-            nodeXML = "%s%s.xml" % (nodepath, nodetype)
-            # Get label
-            stringid = nodes[node]
-            if node != "1":
-                label = lang(stringid)
-                if not label:
-                    label = xbmc.getLocalizedString(stringid)
-            else:
-                label = stringid
-
-            # Set window properties
-            if (mediatype == "homevideos" or mediatype == "photos") and nodetype == "all":
-                # Custom query
-                path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s"
-                        % (tagname, mediatype))
-            elif (mediatype == "homevideos" or mediatype == "photos"):
-                # Custom query
-                path = ("plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=%s"
-                        % (tagname, mediatype, nodetype))
-            elif nodetype == "nextepisodes":
-                # Custom query
-                path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname
-            elif self.kodiversion == 14 and nodetype == "recentepisodes":
-                # Custom query
-                path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname
-            elif self.kodiversion == 14 and nodetype == "inprogressepisodes":
-                # Custom query
-                path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname
-            else:
-                path = "library://video/emby/%s/%s.xml" % (viewid, nodetype)
-            
-            if mediatype == "photos":
-                windowpath = "ActivateWindow(Pictures,%s,return)" % path
-            else:
-                windowpath = "ActivateWindow(Videos,%s,return)" % path
-            
-            if nodetype == "all":
-
-                if viewtype == "mixed":
-                    templabel = "%s - %s" % (tagname, mediatype)
-                else:
-                    templabel = label
-
-                embynode = "Emby.nodes.%s" % indexnumber
-                window('%s.title' % embynode, value=templabel)
-                window('%s.path' % embynode, value=windowpath)
-                window('%s.content' % embynode, value=path)
-                window('%s.type' % embynode, value=mediatype)
-            else:
-                embynode = "Emby.nodes.%s.%s" % (indexnumber, nodetype)
-                window('%s.title' % embynode, value=label)
-                window('%s.path' % embynode, value=windowpath)
-                window('%s.content' % embynode, value=path)
-
-            if mediatype == "photos":
-                # For photos, we do not create a node in videos but we do want the window props
-                # to be created.
-                # To do: add our photos nodes to kodi picture sources somehow
-                continue
-            
-            if xbmcvfs.exists(nodeXML):
-                # Don't recreate xml if already exists
-                continue
-
-            # Create the root
-            if (nodetype == "nextepisodes" or mediatype == "homevideos" or
-                    (self.kodiversion == 14 and nodetype in ('recentepisodes', 'inprogressepisodes'))):
-                # Folder type with plugin path
-                root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2)
-                etree.SubElement(root, 'path').text = path
-                etree.SubElement(root, 'content').text = "episodes"
-            else:
-                root = self.commonRoot(order=node, label=label, tagname=tagname)
-                if nodetype in ('recentepisodes', 'inprogressepisodes'):
-                    etree.SubElement(root, 'content').text = "episodes"
-                else:
-                    etree.SubElement(root, 'content').text = mediatype
-
-                limit = "25"
-                # Elements per nodetype
-                if nodetype == "all":
-                    etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
-                
-                elif nodetype == "recent":
-                    etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
-                    etree.SubElement(root, 'limit').text = limit
-                    rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
-                    etree.SubElement(rule, 'value').text = "0"
-                
-                elif nodetype == "inprogress":
-                    etree.SubElement(root, 'rule', {'field': "inprogress", 'operator': "true"})
-                    etree.SubElement(root, 'limit').text = limit
-
-                elif nodetype == "genres":
-                    etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
-                    etree.SubElement(root, 'group').text = "genres"
-                
-                elif nodetype == "unwatched":
-                    etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
-                    rule = etree.SubElement(root, "rule", {'field': "playcount", 'operator': "is"})
-                    etree.SubElement(rule, 'value').text = "0"
-
-                elif nodetype == "sets":
-                    etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
-                    etree.SubElement(root, 'group').text = "sets"
-
-                elif nodetype == "random":
-                    etree.SubElement(root, 'order', {'direction': "ascending"}).text = "random"
-                    etree.SubElement(root, 'limit').text = limit
-
-                elif nodetype == "recommended":
-                    etree.SubElement(root, 'order', {'direction': "descending"}).text = "rating"
-                    etree.SubElement(root, 'limit').text = limit
-                    rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
-                    etree.SubElement(rule, 'value').text = "0"
-                    rule2 = etree.SubElement(root, 'rule',
-                        attrib={'field': "rating", 'operator': "greaterthan"})
-                    etree.SubElement(rule2, 'value').text = "7"
-
-                elif nodetype == "recentepisodes":
-                    # Kodi Isengard, Jarvis
-                    etree.SubElement(root, 'order', {'direction': "descending"}).text = "dateadded"
-                    etree.SubElement(root, 'limit').text = limit
-                    rule = etree.SubElement(root, 'rule', {'field': "playcount", 'operator': "is"})
-                    etree.SubElement(rule, 'value').text = "0"
-
-                elif nodetype == "inprogressepisodes":
-                    # Kodi Isengard, Jarvis
-                    etree.SubElement(root, 'limit').text = "25"
-                    rule = etree.SubElement(root, 'rule',
-                        attrib={'field': "inprogress", 'operator':"true"})
-
-            try:
-                utils.indent(root)
-            except: pass
-            etree.ElementTree(root).write(nodeXML)
-
-    def singleNode(self, indexnumber, tagname, mediatype, itemtype):
-
-        tagname = tagname.encode('utf-8')
-        cleantagname = utils.normalize_nodes(tagname)
-        nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
-        nodeXML = "%semby_%s.xml" % (nodepath, cleantagname)
-        path = "library://video/emby_%s.xml" % cleantagname
-        windowpath = "ActivateWindow(Videos,%s,return)" % path
-        
-        # Create the video node directory
-        if not xbmcvfs.exists(nodepath):
-            # We need to copy over the default items
-            shutil.copytree(
-                src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
-                dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
-            xbmcvfs.exists(path)
-
-        labels = {
-
-            'Favorite movies': 30180,
-            'Favorite tvshows': 30181,
-            'Favorite episodes': 30182,
-            'channels': 30173
-        }
-        label = lang(labels[tagname])
-        embynode = "Emby.nodes.%s" % indexnumber
-        window('%s.title' % embynode, value=label)
-        window('%s.path' % embynode, value=windowpath)
-        window('%s.content' % embynode, value=path)
-        window('%s.type' % embynode, value=itemtype)
-
-        if xbmcvfs.exists(nodeXML):
-            # Don't recreate xml if already exists
-            return
-
-        if itemtype == "channels":
-            root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
-            etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=0&mode=channels"
-        elif itemtype == "favourites" and mediatype == "episodes":
-            root = self.commonRoot(order=1, label=label, tagname=tagname, roottype=2)
-            etree.SubElement(root, 'path').text = "plugin://plugin.video.emby/?id=%s&mode=browsecontent&type=%s&folderid=favepisodes" %(tagname, mediatype)
-        else:
-            root = self.commonRoot(order=1, label=label, tagname=tagname)
-            etree.SubElement(root, 'order', {'direction': "ascending"}).text = "sorttitle"
-
-        etree.SubElement(root, 'content').text = mediatype
-
-        try:
-            utils.indent(root)
-        except: pass
-        etree.ElementTree(root).write(nodeXML)
-
-    def clearProperties(self):
-
-        log.info("Clearing nodes properties.")
-        embyprops = window('Emby.nodes.total')
-        propnames = [
-        
-            "index","path","title","content",
-            "inprogress.content","inprogress.title",
-            "inprogress.content","inprogress.path",
-            "nextepisodes.title","nextepisodes.content",
-            "nextepisodes.path","unwatched.title",
-            "unwatched.content","unwatched.path",
-            "recent.title","recent.content","recent.path",
-            "recentepisodes.title","recentepisodes.content",
-            "recentepisodes.path","inprogressepisodes.title",
-            "inprogressepisodes.content","inprogressepisodes.path"
-        ]
-
-        if embyprops:
-            totalnodes = int(embyprops)
-            for i in range(totalnodes):
-                for prop in propnames:
-                    window('Emby.nodes.%s.%s' % (str(i), prop), clear=True)
\ No newline at end of file
diff --git a/resources/lib/views.py b/resources/lib/views.py
index d1c0556d..8a2384dc 100644
--- a/resources/lib/views.py
+++ b/resources/lib/views.py
@@ -13,7 +13,7 @@ import xbmcvfs
 
 import read_embyserver as embyserver
 import embydb_functions as embydb
-from utils import window, language as lang
+from utils import window, language as lang, normalize_nodes, indent as xml_indent
 
 #################################################################################################
 
@@ -30,20 +30,34 @@ class Views(object):
     playlists = list()
     views = list()
     sorted_views = list()
+    grouped_views = list()
 
+    media_types = {
+
+        'movies': "Movie",
+        'tvshows': "Series",
+        'musicvideos': "MusicVideo",
+        'homevideos': "Video",
+        'music': "Audio",
+        'photos': "Photo"
+    }
 
     def __init__(self, emby_cursor, kodi_cursor):
         self.emby_cursor = emby_cursor
         self.kodi_cursor = kodi_cursor
 
         self.video_nodes = VideoNodes()
+        self.playlist = Playlist()
         self.emby = embyserver.Read_EmbyServer()
-        self.emby_db = embydb.Embydb_Functions(embycursor)
+        self.emby_db = embydb.Embydb_Functions(emby_cursor)
 
     def _populate_views(self):
         # Will get emby views and views in Kodi
-        self.views = self.emby.getViews(sortedlist=True)
-        for view in self.views:
+        grouped_views = self.emby.get_views()
+        self.grouped_views = grouped_views['Items'] if "Items" in grouped_views else []
+
+        for view in self.emby.getViews(sortedlist=True):
+            self.views.append(view['name'])
             if view['type'] == "music":
                 continue
 
@@ -57,178 +71,105 @@ class Views(object):
     def maintain(self):
         # Compare views to emby
         self._populate_views()
-        view_list = [view['name'] for view in self.all_views]
-        grouped_views = self.emby.get_views()
-        current_views = emby_db.getViews()
+        curr_views = self.emby_db.getViews()
         # total nodes for window properties
-        self.vnodes.clearProperties()
+        self.video_nodes.clearProperties()
 
-        # Set views for supported media type
-        emby_mediatypes = {
-
-            'movies': "Movie",
-            'tvshows': "Series",
-            'musicvideos': "MusicVideo",
-            'homevideos': "Video",
-            'music': "Audio",
-            'photos': "Photo"
-        }
-        for mediatype in ['movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos']:
+        for media_type in ('movies', 'tvshows', 'musicvideos', 'homevideos', 'music', 'photos'):
 
             self.nodes = list() # Prevent duplicate for nodes of the same type
             self.playlists = list() # Prevent duplicate for playlists of the same type
-            # Get media folders from server
-            for folder in [view for view in ordered if view['type'] == mediatype]:
+            # Get media folders from the ordered
+            for folder in self.emby.getViews(media_type, root=True):
 
-                folderid = folder['id']
-                foldername = folder['name']
-                viewtype = folder['type']
+                view_id = folder['id']
+                view_name = folder['name']
+                view_type = folder['type']
 
-                if foldername not in view_list:
+                if view_name not in self.views:
                     # Media folders are grouped into userview
-                    params = {
-                        'ParentId': folderid,
-                        'Recursive': True,
-                        'Limit': 1,
-                        'IncludeItemTypes': emby_mediatypes[mediatype]
-                    } # Get one item from server using the folderid
-                    url = "{server}/emby/Users/{UserId}/Items?format=json"
-                    result = self.doUtils(url, parameters=params)
-                    try:
-                        verifyitem = result['Items'][0]['Id']
-                    except (TypeError, IndexError):
-                        # Something is wrong. Keep the same folder name.
-                        # Could be the view is empty or the connection
-                        pass
-                    else:
-                        for grouped_view in grouped_views:
-                            # This is only reserved for the detection of grouped views
-                            if (grouped_view['Type'] == "UserView" and
-                                grouped_view.get('CollectionType') == mediatype):
-                                # Take the userview, and validate the item belong to the view
-                                if self.emby.verifyView(grouped_view['Id'], verifyitem):
-                                    # Take the name of the userview
-                                    log.info("Found corresponding view: %s %s"
-                                        % (grouped_view['Name'], grouped_view['Id']))
-                                    foldername = grouped_view['Name']
-                                    break
-                        else:
-                            # Unable to find a match, add the name to our sorted_view list
-                            self.sorted_views.append(foldername)
-                            log.info("Couldn't find corresponding grouped view: %s" % self.sorted_views)
-
-                # Failsafe
-                try:
-                    self.sorted_views.index(foldername)
+                    view_name = self._get_grouped_view(media_type, view_id, view_name)
+                
+                try: # Make sure the view is in sorted views before proceeding
+                    self.sorted_views.index(view_name)
                 except ValueError:
-                    self.sorted_views.append(foldername)
+                    self.sorted_views.append(view_name)
 
-                # Get current media folders from emby database
-                view = emby_db.getView_byId(folderid)
-                try:
-                    current_viewname = view[0]
-                    current_viewtype = view[1]
-                    current_tagid = view[2]
+                # Get current media folders from emby database and compare
+                if self.compare_view(media_type, view_id, view_name, view_type):
+                    if view_id in curr_views: # View is still valid
+                        curr_views.remove(view_id)
 
-                except TypeError:
-                    self._add_view(mediatype, folderid, foldername, viewtype)
-
-                else:
-                    log.debug(' '.join((
-
-                        "Found viewid: %s" % folderid,
-                        "viewname: %s" % current_viewname,
-                        "viewtype: %s" % current_viewtype,
-                        "tagid: %s" % current_tagid)))
-
-                    # View is still valid
-                    try:
-                        current_views.remove(folderid)
-                    except ValueError:
-                        # View was just created, nothing to remove
-                        pass
-
-                    # View was modified, update with latest info
-                    if current_viewname != foldername:
-                        log.info("viewid: %s new viewname: %s" % (folderid, foldername))
-                        tagid = self._get_tag(foldername)
-
-                        # Update view with new info
-                        emby_db.updateView(foldername, tagid, folderid)
-
-                        if mediatype != "music":
-                            if emby_db.getView_byName(current_viewname) is None:
-                                # The tag could be a combined view. Ensure there's no other tags
-                                # with the same name before deleting playlist.
-                                self._playlist(mediatype, current_viewname, folderid,
-                                              current_viewtype, True)
-                                # Delete video node
-                                if mediatype != "musicvideos":
-                                    self.vnodes.viewNode(
-                                        indexnumber=None,
-                                        tagname=current_viewname,
-                                        mediatype=mediatype,
-                                        viewtype=current_viewtype,
-                                        viewid=folderid,
-                                        delete=True)
-                            # Added new playlist
-                            self.create_playlist(mediatype, foldername, folderid, viewtype)
-                            # Add new video node
-                            self.create_node(mediatype, foldername, folderid, viewtype)
-
-                        # Update items with new tag
-                        items = emby_db.getItem_byView(folderid)
-                        for item in items:
-                            # Remove the "s" from viewtype for tags
-                            self._update_tag(
-                                current_tagid, tagid, item[0], current_viewtype[:-1])
-                    else:
-                        # Validate the playlist exists or recreate it
-                        if mediatype != "music":
-                            self.create_playlist(mediatype, foldername, folderid, viewtype)
-                            # Create the video node if not already exists
-                            self.create_node(mediatype, foldername, folderid, viewtype)
         # Add video nodes listings
-        self.add_single_node(totalnodes, "Favorite movies", "movies", "favourites")
-        self.add_single_node(totalnodes, "Favorite tvshows", "tvshows", "favourites")
-        self.add_single_node(totalnodes, "Favorite episodes", "episodes", "favourites")
-        self.add_single_node(totalnodes, "channels", "movies", "channels")
+        self.add_single_nodes()
         # Save total
         window('Emby.nodes.total', str(self.total_nodes))
-
         # Remove any old referenced views
-        log.info("Removing views: %s", current_views)
-        for view in current_views:
+        log.info("Removing views: %s", curr_views)
+        for view in curr_views:
             self.remove_view(view)
 
-    def create_node(self, media_type, view_name, view_id, view_type):
+    def _get_grouped_view(self, media_type, view_id, view_name):
+        # Get single item from view to compare
+        result = self.emby.get_single_item(self.media_types[media_type], view_id)
+        try:
+            item = result['Items'][0]['Id']
+        except (TypeError, IndexError):
+            # Something is wrong. Keep the same folder name.
+            # Could be the view is empty or the connection
+            pass
+        else:
+            for view in self.grouped_views:
+                if view['Type'] == "UserView" and view.get('CollectionType') == media_type:
+                    # Take the userview, and validate the item belong to the view
+                    if self.emby.verifyView(view['Id'], item):
+                        log.info("found corresponding view: %s %s", view['Name'], view['Id'])
+                        view_name = view['Name']
+                        break
+            else: # Unable to find a match, add the name to our sorted_view list
+                log.info("couldn't find corresponding grouped view: %s", self.sorted_views)
 
-        if view_name not in self.nodes and media_type not in ('musicvideos', 'music'):
-            index = self.sorted_views.index(view_name)
-            self.video_nodes.viewNode(index, view_name, media_type,view_type, view_id)
-            
-            if view_type == "mixed": # Change the value
-                self.sorted_views[index] = "%ss" % view_name
-            
-            self.nodes.append(view_name)
-            self.total_nodes += 1
-
-    def add_single_node(self, index, tag, media_type, item_type):
-        self.video_nodes.singleNode(index, tag, media_type, item_type)
-        self.total_nodes += 1
+        return view_name
 
     def add_view(self, media_type, view_id, view_name, view_type):
         # Generate view, playlist and video node
         log.info("creating view %s: %s", view_name, view_id)
-        tag_id = self._get_tag(view_name)
+        tag_id = self.get_tag(view_name)
 
-        # Create playlist for the video library
-        self.create_playlist(media_type, view_name, view_id, view_type)
-        # Create the video node
-        self.create_node(media_type, view_name, view_id, view_type)
+        self.add_playlist_node(media_type, view_id, view_name, view_type)
         # Add view to emby database
         self.emby_db.addView(view_id, view_name, view_type, tag_id)
 
+    def compare_view(self, media_type, view_id, view_name, view_type):
+
+        curr_view = self.emby_db.getView_byId(view_id)
+        try:
+            curr_view_name = curr_view[0]
+            curr_view_type = curr_view[1]
+            curr_tag_id = curr_view[2]
+        except TypeError:
+            self.add_view(media_type, view_id, view_name, view_type)
+            return False
+        
+        # View is still valid
+        log.debug("Found viewid: %s viewname: %s viewtype: %s tagid: %s",
+                  view_id, curr_view_name, curr_view_type, curr_tag_id)
+
+        if curr_view_name != view_name:
+            # View was modified, update with latest info
+            log.info("viewid: %s new viewname: %s", view_id, view_name)
+            tag_id = self.get_tag(view_name)
+            # Update view with new info
+            self.emby_db.updateView(view_name, tag_id, view_id)
+            # Delete old playlists and video nodes
+            self.delete_playlist_node(media_type, curr_view_name, view_id, curr_view_type)
+            # Update items with new tag
+            self._update_items_tag(curr_view_type[:-1], view_id, curr_tag_id, tag_id)
+
+        # Verify existance of playlist and nodes
+        self.add_playlist_node(media_type, view_id, view_name, view_type)
+        return True
+
     def remove_view(self, view):
         # Remove any items that belongs to the old view
         items = self.emby_db.get_item_by_view(view)
@@ -236,79 +177,13 @@ class Views(object):
         # TODO: Triage not accessible from here yet
         #self.triage_items("remove", items)
 
-    def create_playlist(self, media_type, view_name, view_id, view_type):
+    def _update_items_tag(self, media_type, view_id, tag, new_tag):
+        items = self.emby_db.getItem_byView(view_id)
+        for item in items:
+            # Remove the "s" from viewtype for tags
+            self._update_tag(tag, new_tag, item[0], media_type)
 
-        if view_name not in self.playlists and media_type in ('movies', 'tvshows', 'musicvideos'):
-
-            self._playlist(media_type, view_name, view_id, view_type)
-            self.playlists.append(view_name)
-
-    @classmethod
-    def _add_playlist(cls, tag, playlist_name, path, media_type):
-        # Using write process since there's no guarantee the xml declaration works with etree
-        special_types = {'homevideos': "movies"}
-        log.info("writing playlist to: %s", path)
-        try:
-            f = xbmcvfs.File(path, 'w')
-        except:
-            log.info("failed to create playlist: %s", path)
-            return False
-        else:
-            f.write(
-                '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
-                '<smartplaylist type="%s">\n\t'
-                    '<name>Emby %s</name>\n\t'
-                    '<match>all</match>\n\t'
-                    '<rule field="tag" operator="is">\n\t\t'
-                        '<value>%s</value>\n\t'
-                    '</rule>'
-                '</smartplaylist>'
-                % (special_types.get(media_type, media_type), playlist_name, tag))
-            f.close()
-        log.info("successfully added playlist: %s" % tag)
-        return True
-
-    @classmethod
-    def _delete_playlist(cls, path):
-        xbmcvfs.delete(path)
-        log.info("successfully removed playlist: %s", path)
-
-    def _playlist(self, media_type, tag, view_id, view_type="", delete=False):
-        # Tagname is in unicode - actions: add or delete
-        tag = tag.encode('utf-8')
-        path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
-
-        if view_type == "mixed":
-            playlist_name = "%s - %s" % (tag, media_type)
-            xsp_path = os.path.join(path, "Emby %s - %s.xsp" % (view_id, media_type))
-        else:
-            playlist_name = tag
-            xsp_path = os.path.join(path, "Emby %s" % view_id)
-
-        # Only add the playlist if it doesn't exist
-        if xbmcvfs.exists(xsp_path):
-            if delete:
-                self._delete_playlist(xsp_path)
-            return True
-
-        elif not xbmcvfs.exists(path):
-            log.info("creating directory: %s", path)
-            xbmcvfs.mkdirs(path)
-
-        return self._add_playlist(tag, playlist_name, xsp_path, media_type)
-
-    def _add_tag(self, tag):
-
-        self.kodi_cursor.execute("select coalesce(max(tag_id),0) from tag")
-        tag_id = self.kodi_cursor.fetchone()[0] + 1
-
-        query = "INSERT INTO tag(tag_id, name) values(?, ?)"
-        self.kodi_cursor.execute(query, (tag_id, tag))
-        log.debug("Create tag_id: %s name: %s", tag_id, tag)
-
-        return tag_id
-
-    def _get_tag(self, tag):
+    def get_tag(self, tag):
         # This will create and return the tag_id
         if KODI in (15, 16, 17):
             # Kodi Isengard, Jarvis, Krypton
@@ -345,6 +220,17 @@ class Views(object):
 
         return tag_id
 
+    def _add_tag(self, tag):
+
+        self.kodi_cursor.execute("select coalesce(max(tag_id),0) from tag")
+        tag_id = self.kodi_cursor.fetchone()[0] + 1
+
+        query = "INSERT INTO tag(tag_id, name) values(?, ?)"
+        self.kodi_cursor.execute(query, (tag_id, tag))
+        log.debug("Create tag_id: %s name: %s", tag_id, tag)
+
+        return tag_id
+
     def _update_tag(self, tag, new_tag, kodi_id, media_type):
 
         log.debug("Updating: %s with %s for %s: %s", tag, new_tag, media_type, kodi_id)
@@ -395,6 +281,110 @@ class Views(object):
                 ))
                 self.kodi_cursor.execute(query, (kodi_id, media_type, tag,))
 
+    def add_playlist_node(self, media_type, view_id, view_name, view_type):
+        # Create playlist for the video library
+        if view_name not in self.playlists and media_type in ('movies', 'tvshows', 'musicvideos'):
+            self.playlist.process_playlist(media_type, view_id, view_name, view_type)
+            self.playlists.append(view_name)
+        # Create the video node
+        self._create_node(media_type, view_id, view_name, view_type)
+
+    def delete_playlist_node(self, media_type, view_id, view_name, view_type):
+
+        if media_type == "music":
+            return
+
+        if self.emby_db.getView_byName(view_name) is None:
+            # The tag could be a combined view. Ensure there's no other tags
+            # with the same name before deleting playlist.
+            self.playlist.process_playlist(media_type, view_id, view_name, view_type, True)
+            # Delete video node
+            if media_type != "musicvideos":
+                self.video_nodes.viewNode(None, view_name, media_type, view_type, view_id, True)
+
+    def _create_node(self, media_type, view_id, view_name, view_type):
+
+        if view_name not in self.nodes and media_type not in ('musicvideos', 'music'):
+            index = self.sorted_views.index(view_name)
+            self.video_nodes.viewNode(index, view_name, media_type,view_type, view_id)
+            
+            if view_type == "mixed": # Change the value
+                self.sorted_views[index] = "%ss" % view_name
+            
+            self.nodes.append(view_name)
+            self.total_nodes += 1
+
+    def add_single_nodes(self):
+        singles = [
+            ("Favorite movies", "movies", "favourites"),
+            ("Favorite tvshows", "tvshows", "favourites"),
+            ("Favorite episodes", "episodes", "favourites"),
+            ("channels", "movies", "channels")
+        ]
+        for args in singles:
+            self._single_node(self.total_nodes, *args)
+
+    def _single_node(self, index, tag, media_type, view_type):
+        self.video_nodes.singleNode(index, tag, media_type, view_type)
+        self.total_nodes += 1
+
+
+class Playlist(object):
+
+    def __init__(self):
+        pass
+
+    def process_playlist(self, media_type, view_id, view_name, view_type, delete=False):
+        # Tagname is in unicode - actions: add or delete
+        tag = view_name.encode('utf-8')
+        path = xbmc.translatePath("special://profile/playlists/video/").decode('utf-8')
+
+        if view_type == "mixed":
+            playlist_name = "%s - %s" % (tag, media_type)
+            xsp_path = os.path.join(path, "Emby %s - %s.xsp" % (view_id, media_type))
+        else:
+            playlist_name = tag
+            xsp_path = os.path.join(path, "Emby %s" % view_id)
+
+        # Only add the playlist if it doesn't exist
+        if xbmcvfs.exists(xsp_path):
+            if delete:
+                self._delete_playlist(xsp_path)
+            return
+
+        elif not xbmcvfs.exists(path):
+            log.info("creating directory: %s", path)
+            xbmcvfs.mkdirs(path)
+
+        self._add_playlist(tag, playlist_name, xsp_path, media_type)
+
+    def _add_playlist(self, tag, name, path, media_type):
+        # Using write process since there's no guarantee the xml declaration works with etree
+        special_types = {'homevideos': "movies"}
+        log.info("writing playlist to: %s", path)
+        try:
+            f = xbmcvfs.File(path, 'w')
+        except:
+            log.info("failed to create playlist: %s", path)
+        else:
+            f.write(
+                '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>\n'
+                '<smartplaylist type="%s">\n\t'
+                    '<name>Emby %s</name>\n\t'
+                    '<match>all</match>\n\t'
+                    '<rule field="tag" operator="is">\n\t\t'
+                        '<value>%s</value>\n\t'
+                    '</rule>'
+                '</smartplaylist>'
+                % (special_types.get(media_type, media_type), name, tag))
+            f.close()
+        log.info("successfully added playlist: %s" % tag)
+
+    @classmethod
+    def _delete_playlist(cls, path):
+        xbmcvfs.delete(path)
+        log.info("successfully removed playlist: %s", path)
+
 
 class VideoNodes(object):
 
@@ -402,7 +392,7 @@ class VideoNodes(object):
     def __init__(self):
         pass
 
-    def commonRoot(self, order, label, tagname, roottype=1):
+    def commonRoot(self, order, label, tagname="", roottype=1):
 
         if roottype == 0:
             # Index
@@ -430,29 +420,10 @@ class VideoNodes(object):
         else:
             dirname = viewid
         
-        path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
+        
         nodepath = xbmc.translatePath(
                     "special://profile/library/video/emby/%s/" % dirname).decode('utf-8')
 
-        # Verify the video directory
-        if not xbmcvfs.exists(path):
-            try:
-                shutil.copytree(
-                    src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
-                    dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
-            except Exception as error:
-                log.error(error)
-
-            xbmcvfs.exists(path)
-
-        emby_path = xbmc.translatePath("special://profile/library/video/emby/index.xml").decode('utf-8')
-        if not emby_path:
-            root = self.commonRoot(order=0, label="Emby", roottype=0)
-            try:
-                utils.indent(root)
-            except: pass
-            etree.ElementTree(root).write(emby_path)
-
         if delete:
             dirs, files = xbmcvfs.listdir(nodepath)
             for file in files:
@@ -460,10 +431,32 @@ class VideoNodes(object):
 
             log.info("Sucessfully removed videonode: %s." % tagname)
             return
+
+        # Verify the video directory
+        path = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
+        if not xbmcvfs.exists(path):
+            try:
+                shutil.copytree(
+                    src=xbmc.translatePath("special://xbmc/system/library/video").decode('utf-8'),
+                    dst=xbmc.translatePath("special://profile/library/video").decode('utf-8'))
+            except Exception as error:
+                log.error(error)
+
+            xbmcvfs.mkdir(path)
+
+        embypath = xbmc.translatePath("special://profile/library/video/emby/").decode('utf-8')
+        if not xbmcvfs.exists(embypath):
+            xbmcvfs.mkdir(embypath)
+            root = self.commonRoot(order=0, label="Emby", roottype=0)
+            try:
+                xml_indent(root)
+            except: pass
+            etree.ElementTree(root).write(os.path.join(embypath, "index.xml"))
+
         # Create the node directory
         if not xbmcvfs.exists(nodepath) and not mediatype == "photos":
             # We need to copy over the default items
-            xbmcvfs.mkdirs(nodepath)
+            xbmcvfs.mkdir(nodepath)
 
         # Create index entry
         nodeXML = "%sindex.xml" % nodepath
@@ -487,7 +480,7 @@ class VideoNodes(object):
             else:
                 root = self.commonRoot(order=0, label=tagname, tagname=tagname, roottype=0)
             try:
-                utils.indent(root)
+                xml_indent(root)
             except: pass
             etree.ElementTree(root).write(nodeXML)
 
@@ -582,10 +575,10 @@ class VideoNodes(object):
             elif nodetype == "nextepisodes":
                 # Custom query
                 path = "plugin://plugin.video.emby/?id=%s&mode=nextup&limit=25" % tagname
-            elif self.kodiversion == 14 and nodetype == "recentepisodes":
+            elif KODI == 14 and nodetype == "recentepisodes":
                 # Custom query
                 path = "plugin://plugin.video.emby/?id=%s&mode=recentepisodes&limit=25" % tagname
-            elif self.kodiversion == 14 and nodetype == "inprogressepisodes":
+            elif KODI == 14 and nodetype == "inprogressepisodes":
                 # Custom query
                 path = "plugin://plugin.video.emby/?id=%s&mode=inprogressepisodes&limit=25"% tagname
             else:
@@ -626,7 +619,7 @@ class VideoNodes(object):
 
             # Create the root
             if (nodetype == "nextepisodes" or mediatype == "homevideos" or
-                    (self.kodiversion == 14 and nodetype in ('recentepisodes', 'inprogressepisodes'))):
+                    (KODI == 14 and nodetype in ('recentepisodes', 'inprogressepisodes'))):
                 # Folder type with plugin path
                 root = self.commonRoot(order=node, label=label, tagname=tagname, roottype=2)
                 etree.SubElement(root, 'path').text = path
@@ -693,14 +686,14 @@ class VideoNodes(object):
                         attrib={'field': "inprogress", 'operator':"true"})
 
             try:
-                utils.indent(root)
+                xml_indent(root)
             except: pass
             etree.ElementTree(root).write(nodeXML)
 
     def singleNode(self, indexnumber, tagname, mediatype, itemtype):
 
         tagname = tagname.encode('utf-8')
-        cleantagname = utils.normalize_nodes(tagname)
+        cleantagname = normalize_nodes(tagname)
         nodepath = xbmc.translatePath("special://profile/library/video/").decode('utf-8')
         nodeXML = "%semby_%s.xml" % (nodepath, cleantagname)
         path = "library://video/emby_%s.xml" % cleantagname
@@ -745,6 +738,30 @@ class VideoNodes(object):
         etree.SubElement(root, 'content').text = mediatype
 
         try:
-            utils.indent(root)
+            xml_indent(root)
         except: pass
         etree.ElementTree(root).write(nodeXML)
+
+    def clearProperties(self):
+
+        log.info("Clearing nodes properties.")
+        embyprops = window('Emby.nodes.total')
+        propnames = [
+        
+            "index","path","title","content",
+            "inprogress.content","inprogress.title",
+            "inprogress.content","inprogress.path",
+            "nextepisodes.title","nextepisodes.content",
+            "nextepisodes.path","unwatched.title",
+            "unwatched.content","unwatched.path",
+            "recent.title","recent.content","recent.path",
+            "recentepisodes.title","recentepisodes.content",
+            "recentepisodes.path","inprogressepisodes.title",
+            "inprogressepisodes.content","inprogressepisodes.path"
+        ]
+
+        if embyprops:
+            totalnodes = int(embyprops)
+            for i in range(totalnodes):
+                for prop in propnames:
+                    window('Emby.nodes.%s.%s' % (str(i), prop), clear=True)