Hi.
Here is the first attempt for an Allocine.fr video information grabber.
Allocine.fr is similar to IMDB, but for french speaking people. So this
grabber works like the video/plugin/imdb.py plugin.

Dishi, i have found one bug (i think) in the util/fxdparser.py : the
setattr method doe not work, i modify it. Moroever, i add a "setcdata"
method to allow cdata just below a root node. My modified fxdparser is
joined.

If you wand to test the plugin (with Freevo CVS only)
1 - save the joined fxdparser.py in src/util
2 - save the joined allocine.py in src/video/plugins
3 - Add a plugin.activate("video.allocine.py) in your config file.
Then you should see a new menu item allowing you to grab video information
from Allocine (again, only for french speaking people).

Current data downloaded :
 - Title
 - Plot
 - Year
 - Picture (if available)

Sylvain.

<french>
Si vous parlez français et que vous utilisez Freevo CVS, pourriez vous
faire des tests complémentaires ??
Merci.
</french>
#if 0 /*
# -----------------------------------------------------------------------
# allocine.py - Plugin for ALLOCINE support
# -----------------------------------------------------------------------
# $Id$
#
# Notes: IMDB plugin. You can add IMDB informations for video items
#        with the plugin
#        activate with plugin.activate('video.allocine')
#        You can also set allocine_search on a key (e.g. '1') by setting
#        EVENTS['menu']['1'] = Event(MENU_CALL_ITEM_ACTION, arg='allocine_search_or_cover_search')
#
# Todo:  - Update existing FXD file
#        - DVD/VCD support (discset ??)
#
# -----------------------------------------------------------------------
# $Log$
# -----------------------------------------------------------------------
# Freevo - A Home Theater PC framework
# Copyright (C) 2002 Krister Lagerstrom, et al. 
# Please see the file freevo/Docs/CREDITS for a complete list of authors.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
# CHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# ----------------------------------------------------------------------- */
#endif

import re
import urllib, urllib2, urlparse
import sys
import codecs
import os
import traceback

import menu
import config
import plugin
import time
from util import htmlenties2txt
from util import fxdparser
from gui.PopupBox import PopupBox
from mmpython.disc.discinfo import cdrom_disc_id

# headers for urllib2
txdata = None
txheaders = {
    'User-Agent': 'freevo (%s)' % sys.platform,
    'Accept-Language': 'fr-fr',
}

class PluginInterface(plugin.ItemPlugin):
    def __init__(self, license=None):
        """Initialise class instance"""

        # these are considered as private variables - don't mess with them unless
        # no other choise is given
        # fyi, the other choice always exists : add a subroutine or ask :)
        if not config.USE_NETWORK:
            self.reason = 'no network'
            return
        plugin.ItemPlugin.__init__(self)

    def initmyself(self):
        self.allocine_id_list = []
        self.allocine_id = None
        self.isdiscset = False
        self.title = ''
        self.info = {}

        self.image = None # full path image filename
        self.image_urls = [] # possible image url list
        self.image_url  = None # final image url

        self.fxdfile = None # filename, full path, WITHOUT extension

        self.append = False
        self.device = None
        self.regexp = None
        self.mpl_global_opt = None
        self.media_id = None
        self.file_opts = []
        self.video = []
        self.variant = []
        self.parts = []
        self.var_mplopt = []
        self.var_names = []

        #image_url_handler stuff
        self.image_url_handler = {}

    def searchAllocine(self, name):
        """name (string), returns id list
        Search for name and returns an id list with tuples:
            (id , name, year, type)"""
        # Clean internal variables
        self.initmyself()

        url = 'http://www.allocine.fr/recherche/rubrique.html?typerecherche=3&motcle=%s' % urllib.quote(name)
        req = urllib2.Request(url, txdata, txheaders)
        searchstring = name

        try:
            response = urllib2.urlopen(req)
        except urllib2.HTTPError, error:
            raise FxdAllocine_Net_Error("IMDB unreachable : " + error)
            exit

        regexp_idarea   = re.compile('.*fichefilm_gen_cfilm=[0-9]*\.html.*', re.I)
        regexp_id       = re.compile('(.*?)\.html"><FONT color=#003399>(.*?)</FONT></A>(.*?)\(([0-9]{4})\)</TD>.*', re.I)
        regexp_id2      = re.compile('(.*?)\.html"><FONT color=#003399>(.*?)</FONT>.*')

        for line in response.read().split("\n"):
            #print line
            m = regexp_idarea.match(line)
            if m:
                #print "Found film id : %s" % line
                split1 = line.split("fichefilm_gen_cfilm=")
                for idfilm in split1:
                    idline = regexp_id.match(idfilm)
                    if idline:
                        #print "Found film id : %s - %s - %s" % (idline.group(1), idline.group(2), idline.group(4))
                        self.allocine_id_list += [ ( idline.group(1), idline.group(2), idline.group(4), 'Movies' ) ]
                    else:
                        idline = regexp_id2.match(idfilm)
                        if idline:
                            #print "Found film id : %s - %s - %s" % (idline.group(1), idline.group(2), 'Unknown')
                            self.allocine_id_list += [ ( idline.group(1), idline.group(2), 'Unknown', 'Movies' ) ]

        return self.allocine_id_list

    def guessAllocine(self, filename, label=False):
        """Guess possible imdb movies from filename. Same return as searchImdb"""
        name = filename

        name  = vfs.basename(vfs.splitext(name)[0])
        name  = re.sub('([a-z])([A-Z])', point_maker, name)
        name  = re.sub('([a-zA-Z])([0-9])', point_maker, name)
        name  = re.sub('([0-9])([a-zA-Z])', point_maker, name.lower())
        name  = re.sub(',', ' ', name)

        if label == True:
            for r in config.IMDB_REMOVE_FROM_LABEL:
                name  = re.sub(r, '', name)

        parts = re.split('[\._ -]', name)
        name = ''
        for p in parts:
            if not p.lower() in config.IMDB_REMOVE_FROM_SEARCHSTRING and \
                   not re.search('[^0-9A-Za-z]', p):
                # originally: not re.search(p, '[A-Za-z]'):
                # not sure what's meant with that
                name += '%s ' % p
        return self.searchAllocine(name)

    def setAllocineId(self, id):
        """id (number)
        Set an allocine number for object, and fetch data"""
        self.allocine_id = id
        #print "Now trying to get %s" % self.allocine_id
        url = 'http://www.allocine.fr/film/fichefilm_gen_cfilm=%s.html' % id
        req = urllib2.Request(url, txdata, txheaders)

        try:
            idpage = urllib2.urlopen(req)
        except urllib2.HTTPError, error:
            raise FxdAllocine_Net_Error("ALLOCINE unreachable" + error)
            return None

        #print "Response : %s" % idpage.read()
        self.parsedata(idpage, id)
        idpage.close()


    def setFxdFile(self, fxdfilename = None, overwrite = False):
        """
        setFxdFile (string, full path)
        Set fxd file to write to, may be omitted, may be an existing file
        (data will be added) unless overwrite = True
        """

        if fxdfilename:
            if vfs.splitext(fxdfilename)[1] == '.fxd':
                self.fxdfile = vfs.splitext(fxdfilename)[0]
            else: self.fxdfile = fxdfilename

        else:
            if self.isdiscset == True:
                self.fxdfile = vfs.join(config.OVERLAY_DIR, 'disc-set',
                                        self.getmedia_id(self.device))
            else:
                self.fxdfile = vfs.splitext(file)[0]

        if overwrite == False:
            try:
                vfs.open(self.fxdfile + '.fxd')
                self.append = True
            except:
                pass
        else:
            self.append = False

        # XXX: add this back in without using parseMovieFile
        # if self.append == True and \
        #    parseMovieFile(self.fxdfile + '.fxd', None, []) == []:
        #     raise FxdAllocine_XML_Error("FXD file to be updated is invalid, please correct it.")

        if not vfs.isdir(vfs.dirname(self.fxdfile)):
            if vfs.dirname(self.fxdfile):
                os.makedirs(vfs.dirname(self.fxdfile))

    def allocine_get_disc_searchstring(self, item):
        name  = item.media.label
        name  = re.sub('([a-z])([A-Z])', point_maker, name)
        name  = re.sub('([a-zA-Z])([0-9])', point_maker, name)
        name  = re.sub('([0-9])([a-zA-Z])', point_maker, name.lower())
        for r in config.IMDB_REMOVE_FROM_LABEL:
            name  = re.sub(r, '', name)
        parts = re.split('[\._ -]', name)

        name = ''
        for p in parts:
            if p:
                name += '%s ' % p
        if name:
            return name[:-1]
        else:
            return ''


    def actions(self, item):
        self.item = item

        if item.type == 'video' and (not item.files or not item.files.fxd_file):
            if item.mode == 'file' or (item.mode in ('dvd', 'vcd') and \
                                       item.info.has_key('tracks') and not \
                                       item.media):
                self.disc_set = False
                return [ ( self.allocine_search , _('Search ALLOCINE for this file'),
                           'allocine_search_or_cover_search') ]

            elif item.mode in ('dvd', 'vcd') and item.info.has_key('tracks'):
                self.disc_set = True
                s = self.allocine_get_disc_searchstring(self.item)
                if s:
                    return [ ( self.allocine_search , _('Search ALLOCINE for [%s]') % s,
                               'allocine_search_or_cover_search') ]

        if item.type == 'dir' and item.media and item.media.mountdir.find(item.dir) == 0:
            self.disc_set = True
            s = self.allocine_get_disc_searchstring(self.item)
            if s:
                return [ ( self.allocine_search , _('Search ALLOCINE for [%s]') % s,
                           'allocine_search_or_cover_search') ]
        return []


    def allocine_search(self, arg=None, menuw=None):
        """
        search allocine for this item
        """
        box = PopupBox(text=_('searching ALLOCINE...'))
        box.show()

        items = []

        try:
            duplicates = []
            if self.disc_set:
                self.searchstring = self.item.media.label
            else:
                self.searchstring = self.item.name

            for id,name,year,type in self.guessAllocine(self.searchstring, self.disc_set):
                try:
                    for i in self.item.parent.play_items:
                        if i.name == name:
                            if not i in duplicates:
                                duplicates.append(i)
                except:
                    pass
                items.append(menu.MenuItem('%s (%s, %s)' % (htmlenties2txt(name), year, type),
                                           self.allocine_create_fxd, (id, year)))
        except:
            box.destroy()
            box = PopupBox(text=_('Unknown error while connecting to ALLOCINE'))
            box.show()
            time.sleep(2)
            box.destroy()
            traceback.print_exc()
            return

        # for d in duplicates:
        #     items = [ menu.MenuItem('Add to "%s"' % d.name,
        #                             self.imdb_add_to_fxd, (d, 'add')),
        #               menu.MenuItem('Variant to "%s"' % d.name,
        #                             self.imdb_add_to_fxd, (d, 'variant')) ] + items

        box.destroy()
        if config.IMDB_AUTOACCEPT_SINGLE_HIT and len(items) == 1:
            self.allocine_create_fxd(arg=items[0].arg, menuw=menuw)
            return

        if items:
            moviemenu = menu.Menu(_('ALLOCINE Query'), items)
            menuw.pushmenu(moviemenu)
            return

        box = PopupBox(text=_('No information available from ALLOCINE'))
        box.show()
        time.sleep(2)
        box.destroy()
        return


    def allocine_menu_back(self, menuw):
        """
        check how many menus we have to go back to see the item
        """
        import directory

        # check if we have to go one menu back (called directly) or
        # two (called from the item menu)
        back = 1
        if menuw.menustack[-2].selected != self.item:
            back = 2

        # maybe we called the function directly because there was only one
        # entry and we called it with an event
        if menuw.menustack[-1].selected == self.item:
            back = 0

        # update the directory
        if directory.dirwatcher:
            directory.dirwatcher.scan()

        # go back in menustack
        for i in range(back):
            menuw.delete_menu()


    def allocine_create_fxd(self, arg=None, menuw=None):
        """
        create fxd file for the item
        """
        box = PopupBox(text=_('getting data...'))
        box.show()

        #if this exists we got a cdrom/dvdrom
        if self.item.media and self.item.media.devicename:
            devicename = self.item.media.devicename
        else:
            devicename = None

        self.setAllocineId(arg[0])

        if self.disc_set:
            self.setDiscset(devicename, None)
        else:
            self.setFxdFile(os.path.splitext(self.item.filename)[0])

        self.writeFxd()
        self.allocine_menu_back(menuw)
        box.destroy()


    def allocine_add_to_fxd(self, arg=None, menuw=None):
        """
        add item to fxd file
        BROKEN, PLEASE FIX
        """

        #if this exists we got a cdrom/dvdrom
        if self.item.media and self.item.media.devicename:
            devicename = self.item.media.devicename
        else: devicename = None

        self.setFxdFile(arg[0].fxd_file)

        if self.item.mode in ('dvd', 'vcd'):
            self.setDiscset(devicename, None)
        else:
            num = len(self.video) + 1
            if arg[1] == 'variant':
                part = makePart('Variant %d' % num, 'f%s' % num)

                if self.variant:
                    part = [ makePart('Variant 1', 'f1'), part ]

                self.setVariants(part)

        self.writeFxd()
        self.imdb_menu_back(menuw)

    def writeFxd(self):
        """Write fxd file"""
        #if fxdfile is empty, set it yourself
        if not self.fxdfile:
            self.setFxdFile()

        try:
            #should we add to an existing file?
            if self.append == True :
                if self.isdiscset == True:
                    self.update_discset()
                else: self.update_movie()
            else:
                #fetch images
                self.fetch_image()
                #should we write a disc-set ?
                if self.isdiscset == True:
                    self.write_discset()
                else:
                    self.write_movie()

            #check fxd
            # XXX: add this back in without using parseMovieFile
            # if parseMovieFile(self.fxdfile + '.fxd', None, []) == []:
            #     raise FxdImdb_XML_Error("""FXD file generated is invalid, please "+
            #                             "post bugreport, tracebacks and fxd file.""")

        except (IOError, FxdAllocine_IO_Error), error:
            raise FxdAllocine_IO_Error('error saving the file: %s' % str(error))


    def setDiscset(self, device, regexp, *file_opts, **mpl_global_opt):
        """
        device (string), regexp (string), file_opts (tuple (mplayer-opts,file)),
        mpl_global_opt (string)
        Set media is dvd/vcd,
        """
        if len(self.video) != 0 or len(self.variant) != 0:
            raise FxdAllocine_XML_Error("<movie> already used, can't use both "+
                                    "<movie> and <disc-set>")

        self.isdiscset = True
        if (not device and not regexp) or (device and regexp):
            raise FxdAllocine_XML_Error("Can't use both media-id and regexp")

        self.device = device
        self.regexp = regexp

        for opts in file_opts:
            self.file_opts += [ opts ]

        if mpl_global_opt and 'mplayer_opt' in mpl_global_opt:
            self.mpl_global_opt = (mpl_global_opt['mplayer_opt'])


    def isDiscset(self):
        """Check if fxd file describes a disc-set, returns 1 for true, 0 for false
        None for invalid file"""
        try:
            file = vfs.open(self.fxdfile + '.fxd')
        except IOError:
            return None

        content = file.read()
        file.close()
        if content.find('</disc-set>') != -1: return 1
        return 0

#------ private functions below .....

    def write_discset(self):
        """Write a <disc-set> to a fresh file"""
        print "Discset not supported for the moment... Sorry"

    def write_fxd_copyright(self, fxd, node):
        fxd.setcdata(node, "The information in this file are from Allocine.fr.\n"+
                           "Please visit http://www.allocine.fr for more informations.\n")
        fxd.add(fxd.XMLnode('source', [('url', "http://www.allocine.fr/";)] ), node, 0)

    def write_fxd_video(self, fxd, node):
        fxd.setattr(node, 'title', self.title )
        fxd.add(fxd.XMLnode('cover-img', (('source', self.image_url), ("test", "test")), self.image ), node, 0)
        videonode = fxd.XMLnode('video')
        fxd.add(videonode, node)
        fxd.add(fxd.XMLnode('file', [('id', 'f1')], os.path.basename(self.item.filename) ), videonode, 0)
        infonode = fxd.XMLnode('info')
        fxd.add(infonode, node)
        if self.info:
            for k in self.info.keys():
                fxd.add(fxd.XMLnode(k, [], self.info[k]), infonode, 0)

    def write_movie(self):
        """Write <movie> to fxd file"""
        try:
            parser = fxdparser.FXD(self.fxdfile + '.fxd')
            parser.set_handler('copyright', self.write_fxd_copyright, 'w', True)
            parser.set_handler('movie', self.write_fxd_video, 'w', True)
            parser.save()
        except:
            print "fxd file %s corrupt" % self.fxdfile
            traceback.print_exc()

    def update_movie(self):
        """Updates an existing file, adds exftra dvd|vcd|file and variant tags"""
        print "Update not supported for the moment... Sorry"

    def update_discset(self):
        """Updates an existing file, adds extra disc in discset"""
        print "Update not supported for the moment... Sorry"

    def parsedata(self, results, id=0):
        """results (allocine html page), allocine_id
        Returns tuple of (title, info(dict), image_urls)"""

        dvd = 0

        regexp_title    = re.compile('.*<TITLE>(.*?)</TITLE>', re.I)
        regexp_plotfull = re.compile('.*<DIV Align=\'justify\'><FONT class="size2">(.*?)</FONT></DIV>.*')
        regexp_plot1    = re.compile('.*<DIV Align=\'justify\'><FONT class="size2">(.*?)<br>$')
        regexp_plot2    = re.compile('(.*?)</FONT></DIV>.*')
        regexp_plot1a   = re.compile('.*<DIV Align=\'justify\'><FONT class="size2">(.*?)$')
        regexp_plot2a   = re.compile('</FONT></DIV>.*')
        regexp_year     = re.compile('.*<A href=\'/film/agenda_gen_semaine=[0-9]{2}/[0-9]{2}/(.*?)\.html')
        regexp_year2    = re.compile('.*<font >\(([0-9]{4})\)</font>.*')
        regexp_url      = re.compile('.*href="(http.*?)"', re.I)
        regexp_pic1     = re.compile('.*<TABLE Border=0 CellPadding=0 CellSpacing=0><TR><TD><IMG Src=\'(.*?)\' Border=0>.*')
        regexp_pic2     = re.compile('.*page=1.html"><IMG Src=\'(.*?)\' Border=0>')

        plot1found  = 0
        plot1afound = 0
        plotfull    = 0

        for line in results.read().split("\n"):
            m = regexp_title.match(line)
            if m:
                self.title = m.group(1)
                #print "Title found : %s" % self.title

            m = regexp_pic1.match(line)
            if m:
                self.image_urls += [ m.group(1) ]
                #print "Image URL 1 : %s" % m.group(1)

            m = regexp_pic2.match(line)
            if m:
                self.image_urls += [ m.group(1) ]
                #print "Image URL 2 : %s" % m.group(1)

            m = regexp_year.match(line)
            if m:
                self.info['year'] = m.group(1)
                #print "Year 1 found : %s" % self.info['year']

            m = regexp_year2.match(line)
            if m:
                self.info['year'] = m.group(1)
                #print "Year 2 found : %s" % self.info['year']

            m = regexp_plotfull.match(line)
            if m:
                plotfull = 1
                self.info['plot'] = m.group(1)
                #print "Synopsis full : %s" % self.info['plot']

            if plot1found:
                m = regexp_plot2.match(line)
                if m:
                    plot1found = 0
                    plotfull   = 1
                else:
                    self.info['plot'] += line
                    #print "Synopsis 2 : %s" % self.info['plot']

            if plotfull == 0:
                m = regexp_plot1.match(line)
                if m:
                    plot1found = 1
                    self.info['plot'] = m.group(1)
                    #print "Synopsis 1 : %s" % self.info['plot']

            if plot1afound:
                m = regexp_plot2a.match(line)
                if m:
                    plot1afound = 0
                    plotfull    = 1
                else:
                    self.info['plot'] += line
                    #print "Synopsis 2a : %s" % self.info['plot']

            if plotfull == 0:
                m = regexp_plot1a.match(line)
                if m:
                    plot1afound = 1
                    self.info['plot'] = m.group(1)
                    #print "Synopsis 1a : %s" % self.info['plot']

        # Clean the plot info
        if plotfull:
            plot = self.info['plot']
            plot = plot.replace("<BR>", "")
            plot = plot.replace("<br>", "")
            self.info['plot'] = plot

        if not id:
            return (self.title, self.info, self.image_urls)


        return (self.title, self.info, self.image_urls)


    def fetch_image(self):
        """Fetch the best image"""
        image_len = 0

        if (len(self.image_urls) == 0): # No images
            return

        for image in self.image_urls:
            try:
                # get sizes of images
                req = urllib2.Request(image, txdata, txheaders)
                r = urllib2.urlopen(req)
                length = int(r.info()['Content-Length'])
                r.close()
                if length > image_len:
                    image_len = length
                    self.image_url = image
            except:
                pass
        if not self.image_url:
            print "Image dowloading failed"
            return

        self.image = (self.fxdfile + '.jpg')

        req = urllib2.Request(self.image_url, txdata, txheaders)
        r = urllib2.urlopen(req)
        i = vfs.open(self.image, 'w')
        i.write(r.read())
        i.close()
        r.close()

        # try to crop the image to avoid borders by imdb
        try:
            import Image
            image = Image.open(filename)
            width, height = image.size
            image.crop((2,2,width-4, height-4)).save(filename)
        except:
            pass

        self.image = vfs.basename(self.image)

        print "Downloaded cover image from %s" % self.image_url
        print "Freevo knows nothing about the copyright of this image, please"
        print "go to %s to check for more informations about private." % self.image_url
        print "use of this image"

class Error(Exception):
    """Base class for exceptions in Allocine_Fxd"""
    def __str__(self):
        return self.message
    def __init__(self, message):
        self.message = message

class FxdAllocine_Error(Error):
    """used to raise exceptions"""
    pass

class FxdAllocine_XML_Error(Error):
    """used to raise exceptions"""
    pass

class FxdAllocine_IO_Error(Error):
    """used to raise exceptions"""
    pass

class FxdAllocine_Net_Error(Error):
    """used to raise exceptions"""
    pass

def point_maker(matching):
    return '%s.%s' % (matching.groups()[0], matching.groups()[1])
#if 0 /*
# -----------------------------------------------------------------------
# fxdparser.py - Parser for fxd files
# -----------------------------------------------------------------------
# $Id: fxdparser.py,v 1.14 2004/02/28 21:04:17 dischi Exp $
#
# Notes:
# Todo:        
#
# -----------------------------------------------------------------------
# $Log: fxdparser.py,v $
# Revision 1.14  2004/02/28 21:04:17  dischi
# unicode fixes
#
# Revision 1.13  2004/01/16 16:23:07  dischi
# made a ugly unicode fix...I will never understand python character encoding
#
# Revision 1.12  2004/01/15 04:33:34  outlyer
# Check for  the filename before trying to read it... without it, I couldn't
# use fxd files which were only in my overlay, but not in the path.
#
# Revision 1.11  2004/01/14 21:20:36  dischi
# fix writing
#
# Revision 1.10  2004/01/14 18:49:37  dischi
# >= is better
#
# Revision 1.9  2004/01/14 18:49:09  dischi
# also dump raw fxd at saving
#
# Revision 1.8  2004/01/14 18:42:47  dischi
# add new helper function
#
# Revision 1.7  2004/01/10 13:17:43  dischi
# o always check if fxd file contains <skin>
# o add filename to fxd object
# o also include info with 'name' when in map
#
# Revision 1.6  2004/01/03 17:43:15  dischi
# OVERLAY_DIR is always used
#
# Revision 1.5  2004/01/01 12:24:18  dischi
# cache fxd files with pickle
#
# Revision 1.4  2003/12/29 22:32:15  dischi
# small speed improvements
#
# Revision 1.3  2003/12/07 19:12:09  dischi
# int support in getattr
#
# Revision 1.2  2003/11/25 19:00:14  dischi
# add support for user data
#
# Revision 1.1  2003/11/23 16:56:28  dischi
# a fxd parser for all kinds of fxd files using callbacks
#
# -----------------------------------------------------------------------
# Freevo - A Home Theater PC framework
# Copyright (C) 2002 Krister Lagerstrom, et al. 
# Please see the file freevo/Docs/CREDITS for a complete list of authors.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of MER-
# CHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# ----------------------------------------------------------------------- */
#endif

import os
import stat
import traceback

# XML support
from xml.utils import qp_xml

import config
import util
import codecs



class XMLnode:
    """
    One node for the FXDtree
    """
    def __init__(self, name, attr = [], first_cdata=None, following_cdata=None):
        self.name = name
        self.attr_list = []
        for name, val in attr:
            self.attr_list.append(((None, name), val))
        self.attrs = self
        self.children = []
        self.first_cdata = first_cdata
        self.following_cdata = following_cdata
        
    def items(self):
        return self.attr_list


class FXDtree(qp_xml.Parser):
    """
    class to parse and write fxd files
    """
    def __init__(self, filename):
        """
        Load the file and parse it. If the file does not exists, create
        an empty <freevo> node.
        """
        qp_xml.Parser.__init__(self)
        self.filename = filename
        if not vfs.isfile(filename):
            self.tree = XMLnode('freevo')
        else:
            self.tree = None
            cache = vfs.getoverlay(filename + '.raw')
            if os.path.isfile(filename) and os.path.isfile(cache) and \
                   os.stat(cache)[stat.ST_MTIME] >= os.stat(filename)[stat.ST_MTIME]:
                self.tree = util.read_pickle(cache)
            if not self.tree:
                f = vfs.open(filename)
                self.tree = self.parse(f)
                f.close()
                if self.tree:
                    util.save_pickle(self.tree, cache)
                

    def add(self, node, parent=None, pos=None):
        """
        add a node to the parent at position pos. If parent is None, the
        <freevo> node fil be taken, if pos is None, the node will be inserted
        at the end of the children list.
        """
        if not parent:
            parent = self.tree
        if pos == None:
            parent.children.append(node)
        else:
            parent.children.insert(pos, node)


    def save(self, filename=None):
        """
        Save the tree
        """
        if not filename:
            filename = self.filename
        if vfs.isfile(filename):
            vfs.unlink(filename)
        f = vfs.codecs_open(filename, 'wb', encoding='utf-8')
        f.write('<?xml version="1.0" encoding="utf-8" ?>\n')
        self._dump_recurse(f, self.tree)

        f.write('\n')
        f.close()

        f = vfs.open(filename)
        self.tree = self.parse(f)
        f.close()
        if self.tree:
            util.save_pickle(self.tree, vfs.getoverlay(filename + '.raw'))



    def _dump_recurse(self, f, elem, depth=0):
        """
        Help function to dump all elements
        """
        if not elem:
            return
        f.write('<' + elem.name)
        for (ns, name), value in elem.attrs.items():
            f.write(u' ' + Unicode(name) + u'="' + Unicode(value) + '"')
        if elem.children or elem.first_cdata:
            if elem.first_cdata == None:
                f.write('>\n  ')
                for i in range(depth):
                    f.write('  ')
            else:
                data = Unicode(elem.first_cdata).replace(u'&', u'&amp;')
                f.write(u'>' + data)
                    
            for child in elem.children:
                self._dump_recurse(f, child, depth=depth+1)
                if child.following_cdata == None:
                    if child == elem.children[-1]:
                        f.write('\n')
                    else:
                        f.write('\n  ')
                    for i in range(depth):
                        f.write('  ')
                else:
                    f.write(child.following_cdata.replace('&', '&amp;'))
            f.write('</%s>' % elem.name)
        else:
            f.write('/>')


class FXD:
    """
    class to help parsing fxd files
    """
    
    class XMLnode(XMLnode):
        """
        a new node
        """
        pass
    
    def __init__(self, filename):
        self.tree = FXDtree(filename)
        self.read_callback  = {}
        self.write_callback = {}
        self.user_data      = {}
        self.is_skin_fxd    = False
        self.filename       = filename

        
    def set_handler(self, name, callback, mode='r', force=False):
        """
        create callbacks for a node named 'name'. Mode can be 'r' when
        reading the node (parse), or 'w' for writing (save). The reading
        callback can return a special callback used for writing this node
        again. If force is true and an element for a write handler doesn't
        exists, it will be created.
        """
        if mode == 'r':
            self.read_callback[name]  = callback
        elif mode == 'w':
            self.write_callback[name] = [ callback, force ]
        else:
            debug('unknown mode %s for fxd handler' % mode, 0)

            
    def parse(self):
        """
        parse the tree and call all the callbacks
        """
        if self.tree.tree.name != 'freevo':
            _debug_('first node not <freevo>')
            return
        for node in self.tree.tree.children:
            if node.name == 'skin':
                self.is_skin_fxd = True
                break
        for node in self.tree.tree.children:
            if node.name in self.read_callback:
                callback = self.read_callback[node.name](self, node)
                if callback:
                    node.callback = callback


    def save(self):
        """
        save the tree and call all write callbacks before
        """
        # create missing but forces elements
        for name in self.write_callback:
            callback, force = self.write_callback[name]
            if force:
                for node in self.tree.tree.children:
                    if node.name == name:
                        break
                else:
                    # forced and missing
                    self.add(XMLnode(name), pos=0)

        # now run all callbacks
        for node in self.tree.tree.children:
            if hasattr(node, 'callback'):
                node.callback(self, node)
            elif node.name in self.write_callback:
                self.write_callback[node.name][0](self, node)

        # and save
        self.tree.save()
        
        
    def get_children(self, node, name, deep=1):
        """
        deep = 0, every deep, 1 = children, 2 = childrens children, etc.
        """
        ret = []
        for child in node.children:
            if deep < 2 and child.name == name:
                ret.append(child)
            if deep == 0:
                ret += self.get_children(child, name, 0)
            elif deep > 1:
                ret += self.get_children(child, name, deep-1)
        return ret


    def get_or_create_child(self, node, name):
        """
        return the first child with name or create it (and add it)
        """
        for child in node.children:
            if child.name == name:
                return child
        child = XMLnode(name)
        self.add(child, node)
        return child
    
        
    def childcontent(self, node, name):
        """
        return the content of the child node with the given name
        """
        for child in node.children:
            if child.name == name:
                return util.format_text(child.textof())
        return ''


    def getattr(self, node, name, default=''):
        """
        return the attribute of the node or the 'default' if the atrribute is not
        set. If 'node' is 'None', it return the user defined data in the fxd
        object.
        """
        r = default
        if node:
            try:
                r = node.attrs[('',name)]
            except KeyError:
                pass
        else:
            try:
                r = self.user_data[name]
            except KeyError:
                pass
        if isinstance(default, int):
            try:
                r = int(r)
            except:
                r = default
        return r

    def setcdata(self, node, cdata):
        if node:
            node.first_cdata = cdata

    def setattr(self, node, name, value):
        """
        sets the attribute of the node or if node is 'None', set user defined data
        for the fxd parser.
        """
        if node:
            node.attr_list.append(((None, name), value))
            #node.attrs[('',name)] = value
        else:
            self.user_data[name] = value


    def gettext(self, node):
        """
        rerurn the text of the node
        """
        return util.format_text(node.textof())


    def parse_info(self, nodes, object, map={}):
        """
        map is a hash, the keys are the names in the fxd file,
        the content the variable names, e.g. {'content': 'description, }
        All tags not in the map are stored with the same name in info.
        """
        if not nodes:
            return

        if hasattr(nodes, 'children'):
            for node in nodes.children:
                if node.name == 'info':
                    nodes = [ node ]
                    break
            else:
                nodes = []
                
        for node in nodes:
            for child in node.children:
                txt = child.textof()
                if not txt:
                    continue
                if child.name in map:
                    object.info[map[child.name]] = util.format_text(txt)
                object.info[child.name] = util.format_text(txt)
                    

    def add(self, node, parent=None, pos=None):
        """
        add an element to the tree
        """
        self.tree.add(node, parent, pos)

Reply via email to