diff --git a/resources/lib/read_embyserver.py b/resources/lib/read_embyserver.py index e2cec77f..b201e807 100644 --- a/resources/lib/read_embyserver.py +++ b/resources/lib/read_embyserver.py @@ -312,19 +312,22 @@ class Read_EmbyServer(): log.info("Increase jump limit to: %s" % jump) return items - def getViews(self, mediatype="", root=False, sortedlist=False): - # Build a list of user views - views = [] - mediatype = mediatype.lower() + def get_views(self, root=False): if not root: url = "{server}/emby/Users/{UserId}/Views?format=json" else: # Views ungrouped url = "{server}/emby/Users/{UserId}/Items?Sortby=SortName&format=json" - result = self.doUtils(url) + return self.doUtils(url) + + def getViews(self, mediatype="", root=False, sortedlist=False): + # Build a list of user views + views = [] + mediatype = mediatype.lower() + try: - items = result['Items'] + items = self.get_views(root)['Items'] except TypeError: log.debug("Error retrieving views for type: %s" % mediatype) else: diff --git a/resources/lib/views.py b/resources/lib/views.py new file mode 100644 index 00000000..d1c0556d --- /dev/null +++ b/resources/lib/views.py @@ -0,0 +1,750 @@ +# -*- coding: utf-8 -*- + +################################################################################################# + +import logging +import shutil +import os +import xml.etree.ElementTree as etree + +import xbmc +import xbmcaddon +import xbmcvfs + +import read_embyserver as embyserver +import embydb_functions as embydb +from utils import window, language as lang + +################################################################################################# + +log = logging.getLogger("EMBY."+__name__) +KODI = int(xbmc.getInfoLabel('System.BuildVersion')[:2]) + +################################################################################################# + + +class Views(object): + + total_nodes = 0 + nodes = list() + playlists = list() + views = list() + sorted_views = list() + + + def __init__(self, emby_cursor, kodi_cursor): + self.emby_cursor = emby_cursor + self.kodi_cursor = kodi_cursor + + self.video_nodes = VideoNodes() + self.emby = embyserver.Read_EmbyServer() + self.emby_db = embydb.Embydb_Functions(embycursor) + + def _populate_views(self): + # Will get emby views and views in Kodi + self.views = self.emby.getViews(sortedlist=True) + for view in self.views: + if view['type'] == "music": + continue + + if view['type'] == "mixed": + self.sorted_views.append(view['name']) + self.sorted_views.append(view['name']) + + log.info("sorted views: %s", self.sorted_views) + self.total_nodes = len(self.sorted_views) + + 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() + # total nodes for window properties + self.vnodes.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']: + + 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]: + + folderid = folder['id'] + foldername = folder['name'] + viewtype = folder['type'] + + if foldername not in view_list: + # 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) + except ValueError: + self.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: + 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") + # 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: + self.remove_view(view) + + def create_node(self, media_type, view_name, view_id, 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_node(self, index, tag, media_type, item_type): + self.video_nodes.singleNode(index, tag, media_type, item_type) + self.total_nodes += 1 + + 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) + + # 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) + # Add view to emby database + self.emby_db.addView(view_id, view_name, view_type, tag_id) + + def remove_view(self, view): + # Remove any items that belongs to the old view + items = self.emby_db.get_item_by_view(view) + items = [i[0] for i in items] # Convert list of tuple to list + # TODO: Triage not accessible from here yet + #self.triage_items("remove", items) + + def create_playlist(self, media_type, view_name, view_id, view_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( + '\n' + '\n\t' + 'Emby %s\n\t' + 'all\n\t' + '\n\t\t' + '%s\n\t' + '' + '' + % (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): + # This will create and return the tag_id + if KODI in (15, 16, 17): + # Kodi Isengard, Jarvis, Krypton + query = ' '.join(( + + "SELECT tag_id", + "FROM tag", + "WHERE name = ?", + "COLLATE NOCASE" + )) + self.kodi_cursor.execute(query, (tag,)) + try: + tag_id = self.kodi_cursor.fetchone()[0] + except TypeError: + tag_id = self._add_tag(tag) + else:# TODO: Remove once Kodi Krypton is RC + query = ' '.join(( + + "SELECT idTag", + "FROM tag", + "WHERE strTag = ?", + "COLLATE NOCASE" + )) + self.kodi_cursor.execute(query, (tag,)) + try: + tag_id = self.kodi_cursor.fetchone()[0] + except TypeError: + self.kodi_cursor.execute("select coalesce(max(idTag),0) from tag") + tag_id = self.kodi_cursor.fetchone()[0] + 1 + + query = "INSERT INTO tag(idTag, strTag) values(?, ?)" + self.kodi_cursor.execute(query, (tag_id, tag)) + log.debug("Create idTag: %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) + + if KODI 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.kodi_cursor.execute(query, (new_tag, kodi_id, media_type, tag,)) + except sqlite3.IntegrityError: + # 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.kodi_cursor.execute(query, (kodi_id, media_type, tag,)) + else:# TODO: Remove once Kodi Krypton is RC + try: + query = ' '.join(( + + "UPDATE taglinks", + "SET idTag = ?", + "WHERE idMedia = ?", + "AND media_type = ?", + "AND idTag = ?" + )) + self.kodi_cursor.execute(query, (new_tag, kodi_id, media_type, tag,)) + except sqlite3.IntegrityError: + # 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.kodi_cursor.execute(query, (kodi_id, media_type, tag,)) + + +class VideoNodes(object): + + + def __init__(self): + pass + + 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) + + 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: + 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)