#!/usr/bin/env python
#
# HTTP server for EyeTV.
#
# Copyright (C) 2003-2005  Andreas Junghans <aj@lucid-cake.net>
#
#    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
#    MERCHANTABILITY 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
#


import fcntl
import gc
import os
import os.path
import popen2
import signal
import re
import socket
import select
import SimpleHTTPServer
import SocketServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
import time
import sys
import threading
import tempfile
import urllib
import shutil
import hotshot

import realtime
import plist
#import videotiming

from mpeg import STREAM_TYPE_UNKNOWN
from mpeg import STREAM_TYPE_MPEG1
from mpeg import STREAM_TYPE_MPEG2_TS
from mpeg import STREAM_SUBTYPE_UNKNOWN
from mpeg import STREAM_SUBTYPE_MPEG4
from mpeg import MINSTREAMSIZE
from mpeg import PCR_STATE_INIT
from mpeg import PCR_STATE_PTS_FOUND
from mpeg import PCR_STATE_FINISHED
from mpeg import NEED_MORE_DATA
from mpeg import getBitrate
from mpeg import MPEG1Handler
from mpeg import MPEG2Handler

F_NOCACHE = 48              # constant for fcntl call to disable caching

BUFFERSTREAMS = 0 #450      # should stay 0 (corrected automagically while streaming)
BUFFERSIZE = MINSTREAMSIZE*BUFFERSTREAMS  # minimum number of available bytes before
                                          # streaming starts
BUNDLESIZE = 1              # number of streams sent in a bundle (MUST be 1 for mixed MPEG-1/MPEG-2
                            # environments, like an EyeTV updated from USB version to FW version)
TRANSCODER_BLOCKSIZE = 65536 #1024     # number of bytes read from a transcoder each time
MAX_DIMENSION_BYTES = 1024*1024  # number of bytes read to determine video dimensions before giving up
MAX_DURATION_BYTES = 1024*1024  # number of bytes read to determine video duration before giving up

LIVETV_DIR = "ffffffffffffffff"
TUNERLOG_FILE = "EyeTVAutoTune.log"
VERSION_DETECT_FILE = "/Library/Preferences/com.elgato.eyetv.devices.plist"
GLOBAL_PREFS_FILE = "/Library/Preferences/com.elgato.eyetv.plist"
EYETV_EXTENSION = ".eyetvi"
MPEG_EXTENSION = ".mpg"
FILEFILTER = re.compile("\\A[0-9a-fA-F]{16}\\" + MPEG_EXTENSION + "\\Z")
DIRFILTER = re.compile("\\A[0-9a-fA-F]{16}\\Z")
IDFILTER = re.compile("[0-9a-fA-F]{16}(?=\\.[^.]*\\Z)")
AUTOTUNE_CHANNEL = re.compile("\\A\\d{2}:\\d{2}:\\d{2}\\.\\d{3} AutoTune channel (\\d+),")
CHANNEL_FOUND = re.compile("\\A\\d{2}:\\d{2}:\\d{2}\\.\\d{3} Tuner status:")
HQ_BITRATE = 2788800

# -----
#outstream = open("/Users/andreasj/Desktop/mpeg4sample.ts", "wb")
# -----

STREAM_QUALITY_SQ = "SQ"
STREAM_QUALITY_HQ = "HQ"
STREAM_QUALITY_M2 = "M2"
STREAM_QUALITY_M4 = "M4"
STREAM_QUALITY_UNKNOWN = "--"

EYETV_VERSION_UNKNOWN = 0
EYETV_VERSION_1_3_1 = 1
EYETV_VERSION_1_3_2 = 2

NOT_MODIFIED = 0
MODIFIED_CURRENT = 1
MODIFIED_EDITED = 2
MODIFIED_UNKNOWN = 3

APPLESCRIPT_PREFIX = """
on prefix(aString)
    if length of aString < 2 then
        return "0" & aString
    else
        return aString
    end if
end prefix
"""

APPLESCRIPT_FORMAT_DATE = """
on format_date(aDate)
        return (aDate as string)
end format_date
"""

APPLESCRIPT_HIDE_EYETV = """
tell application "Finder"
    set visible of some process whose name is "EyeTV" to false
end tell
"""

APPLESCRIPT_PAUSE_EYETV_1 = """
tell application "EyeTV"
    set playback volume to 0
"""
APPLESCRIPT_PAUSE_EYETV_2 = """
    pause
    set playback volume to """
APPLESCRIPT_PAUSE_EYETV_3 = """
end tell
"""

APPLESCRIPT_STOP_EYETV = """
tell application "EyeTV"
    repeat with aWindow in windows
        set windowName to name of aWindow
        if length of windowName >= 8 then
            if ((items 1 through 8) of windowName) as string = "EyeTV - " then
                close aWindow
            end if
        end if
    end repeat
end tell
"""

HTML_OUTPUT = """
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
        <script language="javascript">
            function setChannel() {
                var channelList = document.getElementById("channel");
                var channel = channelList.options[channelList.selectedIndex].text;
                document.getElementById("result").src = "/?setChannel&" + parseInt(channel);
            }
        </script>
    </head>
    <body>
        <form action="html">
            <select id="channel" name="channel" size="10" onClick="setChannel()">
                $channels
            </select>
        </form>
        <iframe id="result" style="visibility:hidden"></iframe>
    </body>
</html>
"""

_exit = 0
_recoverCount = 0
_lock = None
_manageEyeTVLock = None
_fileMap = None
_archivePath = None
_connectedClients = {}
_connectedClientsCount = {}
_eyeTVVersion = EYETV_VERSION_UNKNOWN
_manageEyeTV = 0
_closeTVWindowWhenInactive = 0
_useAltNumbering = 0
_playbackVolume = "0"
_eyeTVIsStopped = 0     # only refers to live streams - there may be a
                        # recording being made even when this is 0
#_overflowDay = 0
#_overflowBits = 0
#_infoCounter = 0


class LoggingLock:
    """
    A Lock class that writes to a log (for debug purposes).
    """

    def __init__(self, filename):
        #self.log = open(os.path.expanduser(filename), "wb")
        self.lock = threading.Lock()


    def acquire(self, caller):
        #self.log.write("[" + caller + "] trying ...\n")
        #self.log.flush()
        self.lock.acquire()
        #self.log.write("[" + caller + "] success\n")
        #self.log.flush()


    def release(self, caller):
        #self.log.write("[" + caller + "] releasing ...\n")
        #self.log.flush()
        self.lock.release()
        #self.log.write("[" + caller + "] released\n")
        #self.log.flush()

    
def hexIdToString(hexId):
    retVal = ""
    for group in hexId.split():
        for index in range(len(group)/2):
            retVal += chr(int(group[index*2:(index + 1)*2], 16))
    return retVal
    

def hideEyeTV():
    """
    Hides EyeTV.
    """
    try:
        os.system("osascript -e '" + APPLESCRIPT_HIDE_EYETV + "'")
    except:
        pass


def stopEyeTV():
    """
    Stops playback in EyeTV (and writing to the TV buffer).
    """
    global _eyeTVIsStopped
    try:
        _manageEyeTVLock.acquire("stopEyeTV")
        manageEyeTV = _manageEyeTV
        closeTVWindowWhenInactive = _closeTVWindowWhenInactive
        if manageEyeTV and closeTVWindowWhenInactive:
            _eyeTVIsStopped = 1
        _manageEyeTVLock.release("stopEyeTV")
        if manageEyeTV and closeTVWindowWhenInactive:
            print "stopping EyeTV (no active clients)"
            sys.stdout.flush()
            os.system("osascript -e '" + APPLESCRIPT_STOP_EYETV + "'")
    except:
        pass
    

def pauseEyeTV(command, playbackVolume):
    """
    Pauses playback in EyeTV after executing the specified command
    (inside a 'tell application "EyeTV"' block).

    After pausing EyeTV, the playback volume is set to playbackVolume.
    """
    global _eyeTVIsStopped
    try:
        _manageEyeTVLock.acquire("pauseEyeTV")
        manageEyeTV = _manageEyeTV
        if manageEyeTV:
            _eyeTVIsStopped = 0
        _manageEyeTVLock.release("pauseEyeTV")
        if manageEyeTV:
            os.system("osascript -e '" + APPLESCRIPT_PAUSE_EYETV_1 + \
                    command + APPLESCRIPT_PAUSE_EYETV_2 + \
                    playbackVolume + APPLESCRIPT_PAUSE_EYETV_3 + "'")
    except:
        pass


def fetchPlaybackVolume():
    """
    Sets the global variable for the current EyeTV playback volume.
    This function doesn't acquire a lock - you should do so before calling it.
    """
    global _playbackVolume
    try:
        command = "osascript -e 'tell application \"EyeTV\" to return playback volume'"
        _playbackVolume = getCommandOutput(command)
    except:
        pass


class Accum:
    """
    Accumulator class (used for performance measurements).
    """

    def __init__(self):
        self.valueSum = 0.0
        self.valueCount = 0


    def add(self, value):
        self.valueSum += value
        self.valueCount += 1


    def sum(self):
        return self.valueSum


    def avg(self):
        if self.valueCount == 0:
            return 0
        return self.valueSum/self.valueCount


def htmlEscape(text):
    retVal = ""
    for char in text:
        if char == "<":
            retVal += "&lt;"
        elif char == ">":
            retVal += "&gt;"
        else:
            retVal += char
    return retVal


def xmlUnescape(text):
    return text.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&")


def getCommandOutput(command):
    """
    Returns the output of a shell command.
    """
    tempname = tempfile.mktemp()
    temphandle = None
    try:
        os.system(command + " >" + tempname)
        temphandle = open(tempname, "r")
        commandOutput = ""
        while 1:
            line = temphandle.readline()
            if line != "":
                commandOutput += line
            else:
                break
        temphandle.close()
        os.remove(tempname)
    except:
        if temphandle != None:
            try:
                temphandle.close()
            except:
                pass
        try:
            os.remove(tempname)
        except:
            pass
        raise
    return commandOutput


class FileInfo:
    """
    Stores information about an MPEG file.
    This class is not thread safe!
    """

    def __init__(self, fileName, isRecording):
        """
        Initializes this instance.
        The isRecording parameter indicates whether this file is part of a
        recording (as opposed to the live TV buffer).
        """
        self.fileName = fileName
        self.isRecording = isRecording
        mo = IDFILTER.search(self.fileName)
        if mo is None:
            raise ValueError, "Cannot extract id from filename '" + str(fileName) + "'"
        self.id = mo.string[mo.start():mo.end()]
        self.recordingPath = os.path.split(self.fileName)[0]
        self.recordingDir = os.path.split(self.recordingPath)[1]
        self.fileNameR = os.path.join(self.recordingPath, self.id + ".eyetvr")
        self.fileNameP = os.path.join(self.recordingPath, self.recordingDir + ".eyetvp")
        self.clients = {}
        self.protectedClients = {}
        self.internalStreamType = STREAM_TYPE_UNKNOWN
        self.internalStreamOffset = 0
        self.width = 0
        self.height = 0
        self.aspectRatio = 0.0
        self.streamType = STREAM_TYPE_UNKNOWN
        self.streamSubtype = STREAM_SUBTYPE_UNKNOWN
        self.byteCount = 0
        self.byteCountIncrement = 0
        self.removed = 0
        self.duration = 0
        self.bitrate = 0
        self.startTime = None
        self.handler = None
        if self.isRecording:
            self.fileHandle = None  # no need to keep a handle open
        else:
            self.fileHandle = open(self.fileName, "rb")
        self.recordingInfo = ""     # additional info about a recording
        self.channelNumber = -1
        self.title = ""
        self.episode = ""
        self.channelName = ""
        self.startDate = ""
        self.timestamp = None
        self.timestampP = None
        self.timestampR = None
    

    def release(self):
        """
        Closes the file handle that is internally opened when initializing.
        """
        if self.fileHandle != None:
            try:
                self.fileHandle.close()
            except:
                pass
            self.fileHandle = None


    def getQuality(self):
        """
        Returns a string with this streams quality
        ("SQ", "HQ", "M2", "M4" or "--" [unknown]).
        """
        if self.streamType == STREAM_TYPE_MPEG1:
            if self.bitrate < HQ_BITRATE:
                quality = STREAM_QUALITY_SQ
            else:
                quality = STREAM_QUALITY_HQ
        elif self.streamType == STREAM_TYPE_MPEG2_TS:
            if self.streamSubtype == STREAM_SUBTYPE_MPEG4:
                quality = STREAM_QUALITY_M4
            else:
                quality = STREAM_QUALITY_M2
        else:
            quality = STREAM_QUALITY_UNKNOWN
        return quality


    def countBytes(self):
        """
        Determines the number of bytes in this file and stores it in
        self.byteCount. If an exception occurs while determining
        the size, 0 is used (to still allow skipping even when the file
        was deleted).

        If self.byteCountIsFinal is not 0, an already stored number is
        not changed.

        In addition to just counting the bytes, the length in seconds of the
        video and its average bitrate is determined and stored.
        """
        if self.removed:
            return NOT_MODIFIED
        try:
            stat = os.stat(self.fileName)
        except:
            # file may be deleted or otherwise unreadable
            self.removed = 1
            return MODIFIED_UNKNOWN
        modified = NOT_MODIFIED
        pModified = 0
        rModified = 0
        pPresent = 0
        rPresent = 0
        if stat.st_mtime != self.timestamp:
            modified = MODIFIED_CURRENT
            self.timestamp = stat.st_mtime
        if self.isRecording:
            try:
                stat2 = os.stat(self.fileNameR)
                rPresent = 1
                if stat2.st_mtime != self.timestampR:
                    modified = MODIFIED_EDITED
                    self.timestampR = stat2.st_mtime
                    rModified = 1
            except:
                pass
            try:
                stat2 = os.stat(self.fileNameP)
                pPresent = 1
                if stat2.st_mtime != self.timestampP:
                    modified = MODIFIED_EDITED
                    self.timestampP = stat2.st_mtime
                    pModified = 1
            except:
                pass

        if modified:
            if self.byteCountIncrement == 0:
                if self.byteCount != 0 and self.byteCount != stat.st_size:
                    self.byteCountIncrement = stat.st_size - self.byteCount
            self.byteCount = stat.st_size
            if self.isRecording:
                if pPresent and pModified:
                    try:
                        eyetvp = plist.readplist(self.fileNameP)
                        info = eyetvp["info"]
                        sourceInfo = info["source info"]
                        try:
                            self.channelNumber = sourceInfo["channel number"]
                        except:
                            pass
                        try:
                            self.title = info["title"]
                        except:
                            pass
                        try:
                            self.channelName = sourceInfo["station name"]
                        except:
                            pass
                        try:
                            service = sourceInfo["service"]
                            channel = _channelsDict[service]
                            self.channelName = channel[channel.index(":")+2:]
                            # if we successfully got here, remove the channel number
                            # as the name we got is probably more reliable
                            self.channelNumber = -1
                        except:
                            pass
                    except:
                        pass
                if rPresent and rModified:
                    try:
                        eyetvr = plist.readplist(self.fileNameR)
                        try:
                            self.startDate = eyetvr["actual start time"].description()
                        except:
                            pass
                        try:
                            self.duration = float(eyetvr["actual duration"])
                            #print "duration: %g" % duration
                            #sys.stdout.flush()
                        except:
                            pass
                        try:
                            self.episode = eyetvr["info"]["title"]
                        except:
                            pass
                    except:
                        pass

                if modified == MODIFIED_EDITED:
                    self.recordingInfo = "@" + self.title.encode("UTF-8") + "\n" + \
                                         "@" + self.channelName.encode("UTF-8") + "\n" + \
                                         "@" + self.startDate + "\n" + \
                                         "@" + str(self.channelNumber) + "\n" + \
                                         "@" + self.episode.encode("UTF-8") + "\n"

            if self.byteCount >= MINSTREAMSIZE:
                fileHandle = None
                if self.internalStreamType == STREAM_TYPE_UNKNOWN:
                    try:
                        fileHandle = open(self.fileName, "rb")
                        fcntl.fcntl(fileHandle, F_NOCACHE, 1)
                        bytes = fileHandle.read(MINSTREAMSIZE)
                        if bytes[0] == "\x47":
                            self.internalStreamType = STREAM_TYPE_MPEG2_TS
                            self.handler = MPEG2Handler
                        else:
                            self.internalStreamType = STREAM_TYPE_MPEG1
                            self.handler = MPEG1Handler
                            self.bitrate = getBitrate(bytes)
                    except:
                        pass
                if self.internalStreamType != STREAM_TYPE_UNKNOWN:
                    # try to determine the video dimensions
                    if self.width == 0 and self.internalStreamOffset < MAX_DIMENSION_BYTES:
                        try:
                            if fileHandle is None:
                                fileHandle = open(self.fileName, "rb")
                                fcntl.fcntl(fileHandle, F_NOCACHE, 1)
                            while self.internalStreamOffset + self.handler.packetSize <= self.byteCount and \
                                    self.internalStreamOffset < MAX_DIMENSION_BYTES:
                                fileHandle.seek(self.internalStreamOffset)
                                bytes = fileHandle.read(self.handler.minSize)
                                videoDimensions = self.handler.getSize(bytes)
                                if videoDimensions == NEED_MORE_DATA:
                                    #print "need more data"
                                    #sys.stdout.flush()
                                    fileHandle.seek(self.internalStreamOffset)
                                    bytes = fileHandle.read(self.handler.packetSize)
                                    videoDimensions = self.handler.getSize(bytes)
                                    if videoDimensions == NEED_MORE_DATA:
                                        #print "need still more data"
                                        #sys.stdout.flush()
                                        self.internalStreamOffset += self.handler.packetSize
                                        continue   # not much we can do
                                if videoDimensions is not None:
                                    self.width = videoDimensions[0]
                                    self.height = videoDimensions[1]
                                    self.aspectRatio = videoDimensions[2]
                                    self.streamType = self.internalStreamType
                                    self.streamSubtype = videoDimensions[3]
                                    print "Size of stream %s: %dx%d, aspect ratio: %.4f" % \
                                          (self.id, self.width, self.height, self.aspectRatio)
                                    sys.stdout.flush()
                                    #print "dimensions determined with internalStreamOffset: " + str(self.internalStreamOffset)
                                    #sys.stdout.flush()
                                    break
                                self.internalStreamOffset += self.handler.packetSize
                            if self.internalStreamOffset >= MAX_DIMENSION_BYTES:
                                print "Cannot determine dimensions of stream %s" % self.id
                                sys.stdout.flush()
                        except:
                            pass

                    # try to determine duration and average bitrate
                    if self.duration <= 0 or modified == MODIFIED_CURRENT:
                        try:
                            if fileHandle is None:
                                fileHandle = open(self.fileName, "rb")
                                fcntl.fcntl(fileHandle, F_NOCACHE, 1)
                            packetCount = self.byteCount/self.handler.packetSize
                            if self.startTime is None:
                                index = 0
                                while index < packetCount and \
                                        index*self.handler.packetSize < MAX_DURATION_BYTES:
                                    fileHandle.seek(index*self.handler.packetSize)
                                    bytes = fileHandle.read(self.handler.minSize)
                                    try:
                                        times = self.handler.getTimes(bytes)
                                    except IndexError:
                                        #print "didn't read enough"
                                        #sys.stdout.flush()
                                        fileHandle.seek(index*self.handler.packetSize)
                                        bytes = fileHandle.read(self.handler.packetSize)
                                        times = self.handler.getTimes(bytes)
                                    self.startTime = times[1]    # PTS
                                    if self.startTime is not None:
                                        #print "start PTS for %s: %g" % (self.id, float(self.startTime))
                                        #sys.stdout.flush()
                                        break
                                    index += 1
                                if index*self.handler.packetSize >= MAX_DURATION_BYTES:
                                    print "Cannot determine duration of stream %s" % self.id
                                    sys.stdout.flush()
                            if self.startTime is not None:
                                endTime = None
                                index = packetCount-1
                                while index >= 0 and \
                                        (packetCount-1-index)*self.handler.packetSize < MAX_DURATION_BYTES:
                                    fileHandle.seek(index*self.handler.packetSize)
                                    bytes = fileHandle.read(self.handler.minSize)
                                    try:
                                        times = self.handler.getTimes(bytes)
                                    except IndexError:
                                        #print "didn't read enough"
                                        #sys.stdout.flush()
                                        fileHandle.seek(index*self.handler.packetSize)
                                        bytes = fileHandle.read(self.handler.packetSize)
                                        times = self.handler.getTimes(bytes)
                                    endTime = times[1]  # PTS
                                    if endTime is not None:
                                        #print "end PTS for %s: %g" % (self.id, float(endTime))
                                        #sys.stdout.flush()
                                        break
                                    index -= 1
                                if (packetCount-1-index)*self.handler.packetSize >= MAX_DURATION_BYTES:
                                    print "Cannot determine duration of stream %s" % self.id
                                    sys.stdout.flush()
                                if endTime != None:
                                    #print "end PTS for %s: %g" % (self.id, float(endTime))
                                    #sys.stdout.flush()
                                    if endTime < self.startTime:
                                        endTime += 0x200000000L # overflow
                                    if self.duration <= 0 or modified == MODIFIED_CURRENT:
                                        self.duration = (endTime - self.startTime)/90000.0
                                        #if self.isRecording:
                                        #    print "manual duration for recording %s: %g" % (self.id, self.duration)
                                        #    sys.stdout.flush()
                        except:
                            pass
                    
                    if self.duration > 0:
                        self.bitrate = self.byteCount*8.0/self.duration

                if fileHandle != None:
                    try:
                        fileHandle.close()
                    except:
                        pass

        return modified


    def __lt__(self, other):
        return self.id < other.id
    
    def __le__(self, other):
        return self.id <= other.id

    def __eq__(self, other):
        if other is None:
            return 0
        return self.id == other.id

    def __ne__(self, other):
        if other is None:
            return 1
        return self.id != other.id

    def __gt__(self, other):
        return self.id > other.id

    def __ge__(self, other):
        return self.id >= other.id

    def __cmp__(self, other):
        return self.id.__cmp__(other)

    def __rcmp__(self, other):
        return self.id.__rcmp__(other)

    def __str__(self):
        return self.fileName

    
class FileMap:
    """
    Stores names of MPEG files and handles of open files. The FileMap is the
    instance responsible for providing an overview over all available files
    (live TV buffer or recordings).
    """

    SEEK_BEGIN = 1
    SEEK_END = 2
    
    
    def resetMembers(self):
        """
        Internal helper method. Do not call from outside this class!
        """
        self.fileHandleMap = {}
        self.fileInfoMap = {}
        self.fileInfoList = []
        self.waitingForRecovery = 0
        self.lastChangeTime = 0
        self.lastLiveAccess = 0
        self.isRecording = 0
        

    def __init__(self):
        """
        Initializes the map.
        """
        self.lock = LoggingLock("~/Library/Logs/CyTV/filemap_lock.log")
        self.clientMap = {}
        self.resetMembers()

    
    def __initClient(self, clientId):
        """
        Introduce a new client. Every single file has changed from
        the client's perspective.
        """
        changes = {}
        for fileInfo in self.fileInfoList:
            changes[fileInfo.id] = fileInfo
        self.clientMap[clientId] = changes

        
    def removeClient(self, clientId):
        """
        Removes a client from the client list. Future info updates
        for this client will contain the full stream list.
        """
        self.lock.acquire("removeClient")
        try:
            del self.clientMap[clientId]
        except:
            pass
        self.lock.release("removeClient")


    def resetClient(self, clientId):
        """
        Resets the list of messages to be sent to a client.
        """
        self.lock.acquire("resetClient")
        self.__initClient(clientId)
        self.lock.release("resetClient")


    def eyetvFileExists(self, mpegFileName):
        """
        Determines whether an additional EyeTV file (.eyetvi) exists for the
        specified mpeg file.

        There are no checks of the specified mpegFileName, so be sure to pass
        a valid one!
        """
        eyetvFileName = mpegFileName[:-len(MPEG_EXTENSION)] + EYETV_EXTENSION
        return os.path.exists(eyetvFileName)
        

    def waitForRecovery(self):
        """
        Wait for recovery of EyeTV - don't scan anything until then.
        """
        self.lock.acquire("waitForRecovery")

        try:
            self.waitingForRecovery = 1

        finally:
            self.lock.release("waitForRecovery")


    def scan(self, archivePath):
        """
        Scans for new files. Also delete files no longer used by EyeTV and
        any clients.
        """
        self.lock.acquire("scan")

        try:
            
            # phase 1: look for new files
            try:
                recordingDirs = os.listdir(archivePath)
            except:
                recordingDirs = []
            recordingDirs = filter(lambda x: DIRFILTER.match(x) != None, recordingDirs)

            # check if we have to recover from EyeTV quitting
            if self.waitingForRecovery:
                if not (LIVETV_DIR in recordingDirs):
                    return
                recordingPath = os.path.join(archivePath, LIVETV_DIR)
                try:
                    recordingFiles = os.listdir(recordingPath)
                except:
                    return
                recordingFiles = filter(lambda x: FILEFILTER.match(x) != None, recordingFiles)
                if len(recordingFiles) == 0:
                    return
                self.waitingForRecovery = 0
            
            for recordingDir in recordingDirs:
                if recordingDir != LIVETV_DIR:
                    recording = 1
                else:
                    recording = 0
                recordingPath = os.path.join(archivePath, recordingDir)
                try:
                    recordingFiles = os.listdir(recordingPath)
                except:
                    recordingFiles = []
                recordingFiles = filter(lambda x: FILEFILTER.match(x) != None, recordingFiles)
                for recordingFile in recordingFiles:
                    fileId = IDFILTER.search(recordingFile)
                    if fileId != None:
                        fileId = fileId.string[fileId.start():fileId.end()]
                        if not self.fileInfoMap.has_key(fileId):
                            mpegFileName = os.path.join(recordingPath, recordingFile)
                            if self.eyetvFileExists(mpegFileName):
                                try:
                                    fileInfo = FileInfo(mpegFileName, recording)
                                    self.fileInfoMap[fileId] = fileInfo
                                except:
                                    pass

            # phase 2: get information about new, changed, or deletes files
            self.fileInfoList = self.fileInfoMap.values()
                # values() returns a copy
            self.fileInfoList.sort()
            timeChanged = 0
            liveAccessChanged = 0
            stopDeleting = 0
            index = 0
            while index < len(self.fileInfoList):
                fileInfo = self.fileInfoList[index]
                if len(fileInfo.protectedClients) > 0:
                    # if there's a client that has requested protection,
                    # don't let files _following_ the place where the
                    # client is be deleted
                    stopDeleting = 1
                    liveAccessChanged = 1
                if not fileInfo.isRecording:
                    if len(fileInfo.clients) > 0:
                        liveAccessChanged = 1
                    elif not stopDeleting:
                        if not self.eyetvFileExists(fileInfo.fileName):
                            try:
                                stat = os.stat(fileInfo.fileName)
                                size = stat.st_size
                            except:
                                size = 0
                            if size != 0:
                                # make sure we never delete 0-length files
                                # (these may be freshly created, with no
                                # accompanying file present yet)
                                fileInfo.removed = 1
                                for changeDict in self.clientMap.itervalues():
                                    changeDict[fileInfo.id] = fileInfo
                                del self.fileInfoMap[fileInfo.id]
                                del self.fileInfoList[index]
                                index -= 1
                                fileInfo.release()
                                try:
                                    os.remove(fileInfo.fileName)
                                except:
                                    pass
                modified = fileInfo.countBytes()
                if fileInfo.streamType == STREAM_TYPE_UNKNOWN:
                    del self.fileInfoList[index]
                    index -= 1
                elif modified:
                    for changeDict in self.clientMap.itervalues():
                        changeDict[fileInfo.id] = fileInfo
                if modified:
                    if modified == MODIFIED_CURRENT:
                        timeChanged = 1
                        self.isRecording = fileInfo.isRecording
                index += 1
            
            currentTime = time.time()
            if timeChanged or self.lastChangeTime == 0 or currentTime < self.lastChangeTime:
                self.lastChangeTime = currentTime
            if liveAccessChanged or self.lastLiveAccess == 0 or currentTime < self.lastLiveAccess:
                self.lastLiveAccess = currentTime

            timeDiff = currentTime - self.lastChangeTime
            if timeDiff > 10:
                # if nothing happened for 10 seconds, reset the
                # recording state and the last live access timer
                self.isRecording = 0
                self.lastLiveAccess = currentTime
            else:
                if self.isRecording:
                    self.lastLiveAccess = currentTime
                else:
                    timeDiff = currentTime - self.lastLiveAccess
                    if timeDiff > 60:
                        # no live access for a minute: stop the live stream
                        self.lastLiveAccess = currentTime   # in case stopping doesn't work immediately
                        stopEyeTV()
            
        finally:
            self.lock.release("scan")


    def isEyeTVRecording(self):
        """
        Returns whether EyeTV is currently recording.

        This method should be called immediately after a scan() so that the
        returned recording state is as current as possible.
        """
        self.lock.acquire("isEyeTVRecording")

        try:
            return self.isRecording
        
        finally:
            self.lock.release("isEyeTVRecording")


    def getCurrentQuality(self):
        """
        Returns the quality of the latest available stream.
        """
        self.lock.acquire("getCurrentQuality")

        try:
            if len(self.fileInfoList) == 0:
                return STREAM_QUALITY_UNKNOWN
            return self.fileInfoList[-1].getQuality()

        finally:
            self.lock.release("getCurrentQuality")


    def openFile(self, fileId, clientId, protect):
        """
        Opens a file and returns a handle to it as well as a handler to deal
        with the file. The file handle is also stored in an internal map.

        If the file is not (any more) contained in the file info map, a
        KeyError is thrown.
        """
        self.lock.acquire("openFile")
        
        try:
            fileInfo = self.fileInfoMap[fileId]
            fileHandle = open(fileInfo.fileName)
            retVal = fcntl.fcntl(fileHandle, F_NOCACHE, 1)
            #fileInfo.clientCount += 1
            if fileInfo.clients.has_key(clientId):
                #print "multiple connections from client " + clientId
                #sys.stdout.flush()
                fileInfo.clients[clientId] += 1
            else:
                fileInfo.clients[clientId] = 1
            if protect:
                fileInfo.protectedClients[clientId] = None
            self.fileHandleMap[fileHandle] = fileInfo
            if fileInfo.streamType == STREAM_TYPE_MPEG1:
                handler = MPEG1Handler
            elif fileInfo.streamType == STREAM_TYPE_MPEG2_TS:
                handler = MPEG2Handler
            else:
                handler = None

            return (fileHandle, handler)

        finally:
            self.lock.release("openFile")
    

    def closeFile(self, fileHandle, clientId):
        """
        Closes a file and removes it from the internal file handle map.
        
        If the file is not present in the file handle map, the file is closed
        anyway. All exceptions are swallowed.
        """
        self.lock.acquire("closeFile")

        try:
            try:
                fileInfo = self.fileHandleMap[fileHandle]
            except:
                fileInfo = None
            if fileInfo != None:
                #fileInfo.clientCount -= 1
                try:
                    fileInfo.clients[clientId] -= 1
                    if fileInfo.clients[clientId] == 0:
                        #print "no more clients for a file"
                        #sys.stdout.flush()
                        del fileInfo.clients[clientId]
                except:
                    pass
                try:
                    del fileInfo.protectedClients[clientId]
                except:
                    pass
                del self.fileHandleMap[fileHandle]

            try:
                fileHandle.close()
            except:
                pass

        finally:
            self.lock.release("closeFile")

    
    def protectFile(self, fileHandle, clientId, protect):
        """
        Changes the protect status of a file (meaning whether files following
        it should be protected from deletion).
        """
        self.lock.acquire("protectFile")

        try:
            try:
                fileInfo = self.fileHandleMap[fileHandle]
            except:
                fileInfo = None
            if fileInfo != None:
                if fileInfo.clients.has_key(clientId):  # sanity check                    
                    if protect:
                        fileInfo.protectedClients[clientId] = None
                    else:
                        try:
                            del fileInfo.protectedClients[clientId]
                        except:
                            pass

        finally:
            self.lock.release("protectFile")
        

    def reset(self, archivePath):
        """
        Resets the whole map. All open handles are forcefully closed, and then
        all internal data structures are cleared. In addition, the live TV
        buffer directory is completely killed.
        
        This method is useful when recovering from EyeTV quitting - and it
        should _only_ be used for that!
        """
        self.lock.acquire("reset")

        try:
            for fileHandle in self.fileHandleMap.keys():
                try:
                    fileHandle.close()
                except:
                    pass
            for fileInfo in self.fileInfoMap.values():
                fileInfo.release()
            try:
                shutil.rmtree(os.path.join(archivePath, LIVETV_DIR))
                print "live tv dir removed because EyeTV quit"
                sys.stdout.flush()
            except:
                pass

        finally:
            self.resetMembers()
            self.lock.release("reset")


    def getAspectRatio(self, fileId):
        """
        Returns the aspect ratio of the specified file.
        """
        self.lock.acquire("getAspectRatio")

        try:
            fileInfo = self.fileInfoMap[fileId]
            return fileInfo.aspectRatio
        finally:
            self.lock.release("getAspectRatio")


    def getBitrate(self, fileId):
        """
        Returns the average bitrate of the specified file.

        If the file is not (any longer) found, a KeyError is thrown.
        """
        self.lock.acquire("getBitrate")

        try:
            fileInfo = self.fileInfoMap[fileId]
            return fileInfo.bitrate
        finally:
            self.lock.release("getBitrate")

    
    def getPacketSize(self, fileId):
        """
        Returns the packet size of the specified filed id.

        If the file is not (any longer) found, a KeyError is thrown.
        """
        self.lock.acquire("getPacketSize")

        try:
            fileInfo = self.fileInfoMap[fileId]
            return fileInfo.handler.packetSize
        finally:
            self.lock.release("getPacketSize")

    
    def getNextFile(self, fileId):
        """
        Returns the id of the file following the specified one. If there is no
        following file, None is returned.

        If the specified id is not (any longer) found, a KeyError is thrown.
        The best strategy to avoid this is to open the next file before
        closing the current one.
        """
        self.lock.acquire("getNextFile")

        try:
            fileInfo = self.fileInfoMap[fileId]
            index = self.fileInfoList.index(fileInfo)
            if index+1 >= len(self.fileInfoList):
                return None
            oldStreamType = fileInfo.streamType
            oldStreamSubtype = fileInfo.streamSubtype
            fileInfo = self.fileInfoList[index+1]
            if fileInfo.streamType != oldStreamType or fileInfo.streamSubtype != oldStreamSubtype:
                if fileInfo.streamType != STREAM_TYPE_UNKNOWN:
                    #print "No matching next stream found"
                    #sys.stdout.flush()
                    raise IOError, "No matching next stream found"
                return None     # stop playing if the stream type is not yet known
            return fileInfo.id
        finally:
            self.lock.release("getNextFile")


    def getFollowingBytes(self, base, maxValue):
        """
        Returns the number of bytes following base (see doc string for seek()
        for details about base). If base is None, counting starts from the
        beginning of the Map.

        If maxValue is not None, counting the bytes stops when maxValue bytes
        are found.
        
        If the file id in the base parameter is not (any longer) found in the
        internal file map, a KeyError is thrown. The best strategy to avoid
        this is to open the next file before closing the current one.

        Don't call this method from outside this class! It's intended for
        internal use only!
        """
        if base is None:
            startIndex = 0
            availableBytes = 0
        else:
            fileInfo = self.fileInfoMap[base[0]]
            startIndex = self.fileInfoList.index(fileInfo)
            availableBytes = -base[1]

        streamType = None
        if maxValue is None:
            for index in range(startIndex, len(self.fileInfoList)):
                fileInfo = self.fileInfoList[index]
                byteCount = fileInfo.byteCount
                if byteCount > 0:
                    if streamType is None:
                        streamType = fileInfo.streamType
                    elif fileInfo.streamType != streamType:
                        break
                    availableBytes += byteCount
        else:
            for index in range(startIndex, len(self.fileInfoList)):
                fileInfo = self.fileInfoList[index]
                byteCount = fileInfo.byteCount
                if byteCount > 0:
                    if streamType is None:
                        streamType = fileInfo.streamType
                    elif fileInfo.streamType != streamType:
                        break
                    availableBytes += byteCount
                    if availableBytes >= maxValue:
                        return maxValue

        return availableBytes
        
        
    def getPrecedingBytes(self, base, maxValue):
        """
        Returns the number of bytes preceding base (see doc string for seek()
        for details about base). If base is None, 0 is returned.

        If maxValue is not None, counting the bytes stops when maxValue bytes
        are found.
        
        If the file id in the base parameter is not (any longer) found in the
        internal file map, a KeyError is thrown. The best strategy to avoid
        this is to open the next file before closing the current one.

        Don't call this method from outside this class! It's intended for
        internal use only!
        """
        if base is None:
            return 0
        
        fileInfo = self.fileInfoMap[base[0]]
        endIndex = self.fileInfoList.index(fileInfo)
        # endIndex is exclusive (the current file is accounted for by using
        # the offset inside it as the start value for the number of available
        # bytes)
        availableBytes = base[1]

        streamType = None
        if maxValue is None:
            for index in range(endIndex-1, -1, -1):
                fileInfo = fileInfoList[index]
                byteCount = fileInfo.byteCount
                if byteCount > 0:
                    if streamType is None:
                        streamType = fileInfo.streamType
                    elif fileInfo.streamType != streamType:
                        break
                    availableBytes += byteCount
        else:
            if availableBytes >= maxValue:
                return maxValue
            for index in range(endIndex-1, -1, -1):
                fileInfo = self.fileInfoList[index]
                byteCount = fileInfo.byteCount
                if byteCount > 0:
                    if streamType is None:
                        streamType = fileInfo.streamType
                    elif fileInfo.streamType != streamType:
                        break
                    availableBytes += byteCount
                    if availableBytes >= maxValue:
                        return maxValue

        return availableBytes
        
        
    def seek(self, base, offset):
        """
        This method returns a pointer to a position in the map as well as the
        offset that was actually applied (may be different from the specified
        one if there is not enough data). To be precise, the return value is a
        tuple consisting of a file id, an offset inside that file, and a value
        specifying the offset that was actually used for seeking (which may
        have taken place across multiple files).

        If the base argument is a tuple of file id and offset, seeking is
        relative to it. If the base argument is FileMap.SEEK_BEGIN, the offset
        is relative to the start of the first file in the map. If the base
        argument is FileMap.SEEK_END, the offset is relative to the end of
        the last file in the map.

        This method pretends that there are BUFFERSIZE less bytes in the map
        than there actually are (a BUFFERSIZE of 0 is OK; this method
        handles this case correctly). This handling ensures that there's
        always a buffer when reading from the various files (except BUFFERSIZE
        is explicitly set to 0).

        If there are no files in the map, (None, 0, 0) is returned.
        
        If the file id in the base parameter is not (any longer) found in the
        internal file map, a KeyError is thrown. The best strategy to avoid
        this is to open the next file before closing the current one.
        """
        self.lock.acquire("seek")

        try:
            if len(self.fileInfoList) == 0:
                return (None, 0, 0)
            
            if base == FileMap.SEEK_END:
                if offset >= 0:
                    actualOffset = 0
                else:
                    actualOffset = offset
                availableBytes = self.getFollowingBytes(None, BUFFERSIZE-actualOffset)
                if BUFFERSIZE-actualOffset > availableBytes:
                    # return a positive value as offset that indicates how
                    # many bytes are missing for fulfilling the seek request
                    return (self.fileInfoList[0].id, 0, BUFFERSIZE-actualOffset-availableBytes)
                currentFileIndex = len(self.fileInfoList)-1
                currentOffset = self.fileInfoList[currentFileIndex].byteCount
                toSeek = actualOffset-BUFFERSIZE
            else:
                if base == FileMap.SEEK_BEGIN:
                    currentFileIndex = 0
                    currentOffset = 0
                else:
                    fileInfo = self.fileInfoMap[base[0]]
                    currentFileIndex = self.fileInfoList.index(fileInfo)
                    currentOffset = base[1]
                if offset > 0:
                    availableBytes = self.getFollowingBytes((self.fileInfoList[currentFileIndex].id, currentOffset),
                                                            BUFFERSIZE+offset)
                    if BUFFERSIZE >= availableBytes:
                        actualOffset = 0
                    elif BUFFERSIZE+offset <= availableBytes:
                        actualOffset = offset
                    else:
                        actualOffset = availableBytes-BUFFERSIZE
                elif offset < 0:
                    availableBytes = self.getPrecedingBytes((self.fileInfoList[currentFileIndex].id, currentOffset),
                                                            -offset)
                    if availableBytes >= -offset:
                        actualOffset = offset
                    else:
                        actualOffset = -availableBytes
                else:
                    actualOffset = 0
                toSeek = actualOffset
            
            # at this point, we know that actualOffset is to be returned as
            # the value by which was actually seeked, that toSeek is the
            # amount we have to physically seek by, and that there is enough
            # room for the seek to succeed
            if toSeek > 0:
                toSeek += currentOffset
                currentOffset = 0
                while toSeek > 0:
                    availableBytes = self.fileInfoList[currentFileIndex].byteCount
                    if availableBytes > toSeek:
                        currentOffset = toSeek
                        toSeek = 0
                    else:
                        toSeek -= availableBytes
                        currentFileIndex += 1
            elif toSeek < 0:
                if currentOffset >= -toSeek:
                    currentOffset += toSeek
                else:
                    toSeek += currentOffset
                    currentFileIndex -= 1
                    while toSeek < 0:
                        availableBytes = self.fileInfoList[currentFileIndex].byteCount
                        if availableBytes >= -toSeek:
                            currentOffset = availableBytes+toSeek
                            toSeek = 0
                        else:
                            toSeek += availableBytes
                            currentFileIndex -= 1
            
            if currentFileIndex == len(self.fileInfoList) and currentOffset == 0:
                currentFileIndex -= 1
                currentOffset = self.fileInfoList[currentFileIndex].byteCount
                
            return (self.fileInfoList[currentFileIndex].id, currentOffset, actualOffset)
            
        finally:
            self.lock.release("seek")


    def calculateTimeDiff(self, pos1, pos2):
        """
        Calculates the difference (in seconds) between two positions in the
        video stream (taking into account possible different bitrates).

        Both pos1 and pos2 must be specified as 2-item tuples of file id and
        offset (in bytes) inside the files. The returned value is pos2-pos1
        (which may be negative).

        If either of the specified filed cannot be found in the map's internal
        data structures, a KeyError is raised.
        """
        self.lock.acquire("calculateTimeDiff")

        try:
            if pos1[0] < pos2[0] or (pos1[0] == pos2[0] and pos1[1] < pos2[1]):
                fileId1, offset1 = pos1
                fileId2, offset2 = pos2
                sign = 1
            else:
                fileId1, offset1 = pos2
                fileId2, offset2 = pos1
                sign = -1

            fileInfo1 = self.fileInfoMap[fileId1]
            index1 = self.fileInfoList.index(fileInfo1)
            if fileId1 == fileId2:
                fileInfo2 = fileInfo1
                index2 = index1
            else:
                fileInfo2 = self.fileInfoMap[fileId2]
                index2 = self.fileInfoList.index(fileInfo2)
            
            timediff = 0.0
            while index2 > index1:
                if fileInfo2.bitrate != 0:
                    timediff += offset2 * 8.0 / fileInfo2.bitrate
                index2 -= 1
                fileInfo2 = self.fileInfoList[index2]
                offset2 = fileInfo2.byteCount
            if fileInfo1.bitrate != 0:
                timediff += (offset2-offset1) * 8.0 / fileInfo1.bitrate

            return timediff*sign

        finally:
            self.lock.release("calculateTimeDiff")

        
    def writeInfo(self, handle, clientId):
        """
        Writes the information contained in the map to the specified file
        handle.

        The format is <id>,<byteCount>,<seconds>,<recorded>,<quality> with
        each entry in its own line.

        <id> is the unique file id
        <byteCount> is the number of bytes in the file
        <seconds> is the playing time of the file
        <recorded> is either 0 (the file is from the live buffer) or 1
                   (the file is permanently recorded)
        <quality> is either SQ (for standard quality) or HQ (for high quality)
                  or M2 (for MPEG2)

        Information is sent as deltas, i.e. only information about
        new or changed streams is sent with each request. Deleted streams
        are sent like this:

        -<id>
        """
        self.lock.acquire("writeInfo")

        try:
            if not self.clientMap.has_key(clientId):
                self.__initClient(clientId)
            for fileInfo in self.clientMap[clientId].itervalues():
                if fileInfo.removed:
                    handle.write("-" + str(fileInfo.id) + "\n")
                else:
                    quality = fileInfo.getQuality()
                    handle.write(str(fileInfo.id) + "," + str(fileInfo.byteCount) + "," + str(fileInfo.duration) + "," + str(fileInfo.isRecording) + "," + str(quality) + "\n")
                    if fileInfo.isRecording:
                        handle.write(fileInfo.recordingInfo)
            self.clientMap[clientId] = {}
        finally:
            self.lock.release("writeInfo")

        
    def dumpInfo(self):
        """
        Prints information about all files in the map to stdout.
        """
        self.lock.acquire("dumpInfo")

        try:
            for fileInfo in self.fileInfoList:
                print fileInfo.id + ": " + fileInfo.fileName

        finally:
            self.lock.release("dumpInfo")


    def getLastChangeTime(self):
        """
        Returns the time of the last change in the file map.
        """
        self.lock.acquire("getLastChangeTime")
        retVal = self.lastChangeTime
        self.lock.release("getLastChangeTime")
        return retVal


    def getCurrentByteCountIncrement(self):
        """
        Returns the size by which a live buffer file typically increases at once.
        """
        self.lock.acquire("getCurrentByteCountIncrement")
        retVal = 0
        if len(self.fileInfoList) > 0:
            retVal = self.fileInfoList[-1].byteCountIncrement
        self.lock.release("getCurrentByteCountIncrement")
        return retVal

        
class ServerExitException(Exception):
    """
    Exception thrown when the server is quitting (needed for clean shutdown).
    """
    pass


class Writer:
    """
    A writer that takes data from a transcoder and writes it to our output
    stream.
    """

    def __init__(self, parent):
        self.parent = parent


    def closeHandle(self):
        """
        Closes the file handles from the transcoder.
        """
        try:
            self.parent.transcoder.fromchild.close()
        except:
            pass


    def __call__(self):
        """
        Called when the thread is started.
        """
        parent = self.parent
        transcoder = parent.transcoder
        try:
            while 1:
                data = transcoder.fromchild.read(TRANSCODER_BLOCKSIZE)
                if data == "":
                    # EOF; this happens when the transcoder is killed
                    self.closeHandle()
                    break
                if not parent.headerWritten:
                    parent.headerWritten = 1
                    parent.wfile.write("HTTP/1.1 200 OK\r\n")
                    parent.send_header("Connection", "close")
                    #parent.send_header("Transfer-Encoding", "chunked")
                    #parent.send_header("Content-Type", "video/mpeg")
                    #parent.send_header("Content-Length", "6000000")
                    parent.end_headers()
                    parent.wfile.flush()    # make sure 8k alignment really works
                parent.wfile.write(data)
                #parent.wfile.flush()
        except:
            exc_info = sys.exc_info()
            self.closeHandle()
            _lock.acquire("Writer.__call__")
            self.parent.writerThreadError = exc_info
            _lock.release("Writer.__call__")
            

class ErrLogger:
    """
    A logger that reads the error stream from a transcoder and writes it
    to stdout.
    """

    def __init__(self, parent):
        self.parent = parent


    def closeHandle(self):
        """
        Closes the error stream handle.
        """
        try:
            self.parent.transcoder.childerr.close()
        except:
            pass


    def __call__(self):
        """
        Called when the thread is started.
        """
        parent = self.parent
        transcoder = parent.transcoder
        try:
            while 1:
                error = transcoder.childerr.readline()
                if error == "":
                    # EOF; this happens when the transcoder is killed
                    self.closeHandle()
                    break
                sys.stdout.write("[transcoder] " + error);
                sys.stdout.flush()
            #print "end of reading strerr from transcoder"
            #sys.stdout.flush()
        except:
            self.closeHandle()
    
    
class MyHTTPServer(SocketServer.ThreadingTCPServer):
    """
    HTTP server class (based on BaseHTTPServer.py; most notably, it's
    derived from ThreadingTCPServer to handle more than one request.
    """

    allow_reuse_address = 1    # Seems to make sense in testing environment
    daemon_threads = 1         # allows (somewhat) clean shutdown


    def server_bind(self):
        """Override server_bind to store the server name."""
        SocketServer.TCPServer.server_bind(self)
        host, port = self.socket.getsockname()
        self.server_name = socket.getfqdn(host)
        self.server_port = port
    

    def get_request(self):
        """
        Overrides the default implementation by checking the socket with a
        timeout (better for quitting cleanly).
        """
        socklist = [self.socket]
        emptylist = []
        while 1:
            ready = select.select(socklist, emptylist, emptylist, 1.0)
            # short timeout for fast quitting
            _lock.acquire("MyHTTPServer.get_request")
            localExit = _exit
            _lock.release("MyHTTPServer.get_request")
            if localExit:
                raise ServerExitException
            if len(ready[0]) > 0:
                return SocketServer.ThreadingTCPServer.get_request(self)
        
        
    def serve_forever(self):
        """
        Handles request forever (at least until we quit).
        """
        while 1:
            try:
                self.handle_request()
            except ServerExitException:
                self.server_close()
                break
        
    

class MyHTTPRequestHandlerCommon(SimpleHTTPRequestHandler):
    """
    Handler for HTTP requests. There is one instance of this per client.
    """

    #def setup(self):
    #    #print "setup"
    #    self.connection = self.request
    #    self.rfile = self.connection.makefile('rb', -1)
    #    self.wfile = self.connection.makefile('wb', 8192)

        
    def writeStream(self, bytes):
        """
        Writes the specified bytes to the output stream after making any
        necessary adjustments to the timing parameters.

        The "bytes" parameter must contain valid streams and must not be
        None or "".
        """
        if self.transcoder is not None:
            #_lock.acquire("MyHTTPRequestHandlerCommon.writeStream 1")
            self.safeForChanging = 1
            #_lock.release("MyHTTPRequestHandlerCommon.writeStream 1")
            # not necessary here
            self.transcoder.tochild.write(bytes)
            #self.transcoder.tochild.flush()
            #print "after write to child"
            #self.bytesWritten += len(buffer)
            #if self.bytesWritten > 10000000:
            #    self.killTranscoder()
            #    self.startTranscoder()
            #    self.bytesWritten = 0
        else:
            times = self.currentHandler.getTimes(bytes)
            cr = times[0]
            pts = times[1]
            dts = times[2]
            if cr != None:
                cr += self.clockOffset
            if pts != None:
                pts += self.clockOffset
            if dts != None:
                dts += self.clockOffset
            buffer = self.currentHandler.setTimes(bytes, cr, pts, dts, times[3], self)
            #_lock.acquire("MyHTTPRequestHandlerCommon.writeStream 2")
            self.safeForChanging = 1
            #_lock.release("MyHTTPRequestHandlerCommon.writeStream 2")
            # not necessary here
            if not self.headerWritten:
                self.headerWritten = 1
                self.wfile.write("HTTP/1.1 200 OK\r\n")
                self.send_header("Connection", "close")
                #self.send_header("Transfer-Encoding", "chunked")
                self.send_header("Content-Type", "video/mpeg")
                #self.send_header("Content-Length", "6000000")
                self.end_headers()
                self.wfile.flush()    # make sure 8k alignment really works
            self.wfile.write(buffer)
        _lock.acquire("MyHTTPRequestHandlerCommon.writeStream 3")
        self.safeForChanging = 0
        _lock.release("MyHTTPRequestHandlerCommon.writeStream 3")


    def readAndWriteStream(self):
        """
        Reads a bundle of streams and writes them to the current
        output handle.

        Exceptions while reading and writing are propagated.

        It is assumed that all of these properties are valid and not None:

            - self.currentFileHandle
            - self.currentFile
            - self.currentOffset

        You have to call findStartPoint() to initialize these. After their
        values or valid, they are automatically updated by readAndWriteStream().
        """
        if self.transcoder is None and not self.headerWritten:
            self.safeForChanging = 1
            try:
                self.headerWritten = 1
                self.wfile.write("HTTP/1.1 200 OK\r\n")
                self.send_header("Connection", "close")
                #self.send_header("Transfer-Encoding", "chunked")
                self.send_header("Content-Type", "video/mpeg")
                #self.send_header("Content-Length", "6000000")
                self.end_headers()
                self.wfile.flush()    # make sure 8k alignment really works
            finally:
                _lock.acquire("MyHTTPRequestHandlerCommon.readAndWriteStream 1")
                self.safeForChanging = 0
                _lock.release("MyHTTPRequestHandlerCommon.readAndWriteStream 1")
            
        # #####
        #try:
        #    self.liveSocket
        #except:
        #    self.liveSocket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        #    self.liveSocket.connect("/tmp/CyTV_serverSocket")
        #data = ""
        #while len(data) < 188*43:
        #    data += self.liveSocket.recv(188*43-len(data))
        #self.wfile.write(data)
        #return
        # *****
        oldOffset = self.currentOffset
        try:
            if self.transcoder is not None:
                lenRead = self.currentHandler.readAndWritePacket( \
                            self, self.currentFileHandle, \
                            self.transcoder.tochild, None)
            else:
                lenRead = self.currentHandler.readAndWritePacket( \
                            self, self.currentFileHandle, \
                            self.wfile, self.clockOffset)
        finally:
            _lock.acquire("MyHTTPRequestHandlerCommon.readAndWriteStream 2")
            self.safeForChanging = 0
            _lock.release("MyHTTPRequestHandlerCommon.readAndWriteStream 2")

        if self.currentOffset == oldOffset: self.currentOffset += lenRead
        if lenRead == 0:
            #print "currentFile:", self.currentFile
            #print "currentOffset:", self.currentOffset
            #print "actual position:", self.currentFileHandle.tell()
            nextFile = _fileMap.getNextFile(self.currentFile)
            if nextFile is None:
                # no next file (yet): don't move, and return an empty
                # result
                self.currentFileHandle.seek(self.currentOffset)
                #if 1:
                if not self.streamingCacheClient or self.cacheEmpty:
                    self.cacheEmpty = 0
                    if self.transcoder is not None:
                        self.seekRelative(-2)
                        #self.clockOffset += 180000
                    else:
                        self.clockOffset += 180000   # delay playback for 3 secs
                    print "delaying playback"
                    sys.stdout.flush()
                time.sleep(1)
            else:
                try:
                    newHandle, newHandler = _fileMap.openFile(nextFile, self.clientId, self.protect)
                    error = 0
                except:
                    # the file may have been taken away right
                    # under us; do nothing (the next file in the
                    # next round will be a different one)
                    error = 1
                if not error:
                    _fileMap.closeFile(self.currentFileHandle, self.clientId)
                    self.currentFileHandle = newHandle
                    self.currentHandler = newHandler
                    self.currentFile = nextFile
                    self.currentOffset = 0
                    if self.transcodeOptions is None:
                        self.detectPCRState = PCR_STATE_INIT    # redetect whether
                                                                # generating PCRs
                                                                # is necessary
                        self.pcrId = 0
                        #print "PCR search init for " + self.currentFile
                        #sys.stdout.flush()
                    #bytes2 = self.currentFileHandle.read(readSize-lenRead)
                    #self.currentOffset += len(bytes2)
                    #bytes += bytes2
                    # commented out; don't try to read anything in this case
                    # (there may not be enough bytes yet); on the next round,
                    # reading will continue normally
        #if bytes[12:16] == "\x00\x00\x01\xc0":
        #    print "A",
        #elif bytes[12:16] == "\x00\x00\x01\xe0":
        #    print "V",
        #elif bytes[12:16] == "\x00\x00\x01\xbe":
        #    print "P",
        #else:
        #    print "U",
        #return bytes


    def readBlock(self):
        """
        Reads a bundle of streams.

        Exceptions are propagated.
        
        It is assumed that all of these properties are valid and not None:

            - self.currentFileHandle
            - self.currentFile
            - self.currentOffset

        You have to call findStartPoint() to initialize these. After their
        values or valid, they are automatically updated by readBlock().
        """
        bytes = self.readStream()
        if len(bytes) > 0:
            self.writeStream(bytes)

    
    def seekRelative(self, skipTime):
        """
        Changes the current position in the video stream. The skipTime
        parameter contains the number of seconds to skip (either negative for
        skipping back or positive for skipping forward).

        If seeking fails, the current position is not changed.
        """
        #print "seekRelative: " + str(skipTime)
        #sys.stdout.flush()
        bitrate = _fileMap.getBitrate(self.currentFile)
        if skipTime != 0 and bitrate != 0:
            streamSize = self.currentHandler.packetSize
            if skipTime < 0:
                skipStreams = round((bitrate/8.0) * (-skipTime) / streamSize)
                toSeek = -skipStreams*streamSize
                #print "toSeek: " + str(toSeek)
                #sys.stdout.flush()
            else:
                # Make sure we stay behind the end of the stream.
                # Otherwise, the cache might run dry.
                skipStreamsOrig = round((bitrate/8) * skipTime / streamSize)
                skipStreams = round(((bitrate/8) * (skipTime-self.bufferSeconds) - self.bufferBytes) / streamSize)
                toSeek = skipStreams*streamSize
            (fileId, currentOffset, actualOffset) = \
                    _fileMap.seek((self.currentFile, self.currentOffset), toSeek)
            #print "currentOffset:", currentOffset, "actualOffset:", actualOffset
            #sys.stdout.flush()
            if actualOffset != 0 and currentOffset%streamSize == 0: # sanity check for currentOffset
                actualSkipTime = actualOffset*8.0/bitrate
                if skipTime > 0:
                    if actualSkipTime < 3:
                        return
                    if actualSkipTime > skipTime:
                        (fileId, currentOffset, actualOffset) = \
                                _fileMap.seek((self.currentFile, self.currentOffset), \
                                              skipStreamsOrig*streamSize)
                        if actualOffset == 0 or currentOffset%streamSize != 0:
                            return
                        actualSkipTime = actualOffset*8.0/bitrate
                try:
                    newHandle, newHandler = _fileMap.openFile(fileId, self.clientId, self.protect)
                    _fileMap.closeFile(self.currentFileHandle, self.clientId)
                    self.currentFileHandle = newHandle
                    self.currentHandler = newHandler
                    self.currentFile = fileId
                    if self.transcodeOptions is None:
                        self.detectPCRState = PCR_STATE_INIT
                    self.currentOffset = currentOffset
                    self.clockOffset -= long(round(actualSkipTime*90000))
                    #if self.clockOffset < 0:
                    #    self.clockOffset = 0
                    #print "trace 1"
                    #sys.stdout.flush()
                    self.currentFileHandle.seek(self.currentOffset)
                    #print "trace 2"
                    #sys.stdout.flush()
                except:
                    pass
        
        
    def seekAbsolute(self, fileId, offset):
        """
        Changes the current position in the video stream to the specified file
        and offset.

        If seeking fails, the current position is not changed.
        """
        try:
            if fileId == self.currentFile:
                streamSize = self.currentHandler.packetSize
            if fileId != self.currentFile:
                newHandle, newHandler = _fileMap.openFile(fileId, self.clientId, self.protect)
                streamSize = newHandler.packetSize
            actualOffset = offset - offset % streamSize
            timediff = _fileMap.calculateTimeDiff((self.currentFile, self.currentOffset), (fileId, actualOffset))
            if fileId != self.currentFile:
                _fileMap.closeFile(self.currentFileHandle, self.clientId)
                self.currentFileHandle = newHandle
                self.currentFile = fileId
                if self.transcodeOptions is None:
                    self.detectPCRState = PCR_STATE_INIT
                self.currentHandler = newHandler
            self.currentOffset = actualOffset
            self.clockOffset -= long(round(timediff * 90000))
            #if self.clockOffset < 0:
            #    self.clockOffset = 0
            #self.clockOffset += 60 * 90000
            self.currentFileHandle.seek(self.currentOffset)
            #print "seekAbsolute() successful; clockOffset:", self.clockOffset
            #sys.stdout.flush()
        except:
            #print "Exception in seekAbsolute()"
            #sys.stdout.flush()
            pass


    def findStartPoint(self, streamId=None):
        """
        Finds a point in the video files to start streaming.

        If CyTV is configured to manage EyeTV, playback is automatically
        paused and a TV window is opened in EyeTV.
        """
        self.currentFile = None
        if self.transcodeOptions is None:
            self.detectPCRState = PCR_STATE_INIT
        if self.currentFileHandle != None:
            _fileMap.closeFile(self.currentFileHandle, self.clientId)
            self.currentFileHandle = None
        self.currentOffset = 0
        #self.clockOffset = 0    #0x200000000L-(60*90000) #(24*3600-60)*90000
        #_fileMap.dumpInfo()
        #sys.stdout.flush()
        _fileMap.scan(_archivePath)
        if streamId is not None:
            try:
                (fileId, currentOffset, actualOffset) = _fileMap.seek((streamId, 0), 0)
                self.currentFileHandle, self.currentHandler = \
                        _fileMap.openFile(fileId, self.clientId, self.protect)
                self.currentFile = fileId
                if self.transcodeOptions is None:
                    self.detectPCRState = PCR_STATE_INIT
                return
            except:
                pass
        _manageEyeTVLock.acquire("MyHTTPRequestHandlerCommon.findStartPoint")
        manageEyeTV = _manageEyeTV
        playbackVolume = _playbackVolume
        eyeTVIsStopped = _eyeTVIsStopped
        _manageEyeTVLock.release("MyHTTPRequestHandlerCommon.findStartPoint")
        if manageEyeTV:
            lastChangeTime = _fileMap.getLastChangeTime()
            pauseEyeTV("play", playbackVolume)
            while 1:
                # wait until something has changed in the
                # file map; this ensures that we get the
                # actual live stream (important in case the
                # last recording is in a different quality
                # than the live stream)
                _fileMap.scan(_archivePath)
                if lastChangeTime == _fileMap.getLastChangeTime():
                    time.sleep(1)
                else:
                    break
            if eyeTVIsStopped:
                # EyeTV writes some old content after unpausing, skip that
                time.sleep(10)
        while 1:
            byteCountIncrement = _fileMap.getCurrentByteCountIncrement()
            if byteCountIncrement > 0:
                break
            time.sleep(1)
        if self.transcodeOptions is None:
            self.bufferBytes = -self.cacheSize*1024-byteCountIncrement
            self.bufferSeconds = 0
        else:
            self.bufferBytes = 0
            self.bufferSeconds = -(self.cacheSize + byteCountIncrement/1024.0)*8.0/((self.videoBitrate + self.audioBitrate)/1.0)
            print "seeking", self.bufferSeconds, "seconds"
            sys.stdout.flush()
        while 1:
            (fileId, currentOffset, actualOffset) = _fileMap.seek(_fileMap.SEEK_END, self.bufferBytes)
            #print fileId, currentOffset, actualOffset
            #sys.stdout.flush()
            if actualOffset == self.bufferBytes and fileId != None:
                try:
                    # adjust for the packet size
                    packetSize = _fileMap.getPacketSize(fileId)
                    tooMuch = (-self.bufferBytes)%packetSize
                    if tooMuch != 0:
                        (fileId, currentOffset, actualOffset) = _fileMap.seek((fileId, currentOffset), tooMuch)

                    # now open the file
                    self.currentFileHandle, self.currentHandler = \
                            _fileMap.openFile(fileId, self.clientId, self.protect)
                    self.currentFile = fileId
                    if self.transcodeOptions is None:
                        self.detectPCRState = PCR_STATE_INIT
                    streamSize = self.currentHandler.packetSize
                    self.currentOffset = (currentOffset / streamSize) * streamSize
                    self.currentFileHandle.seek(self.currentOffset)
                    if self.bufferSeconds != 0:
                        self.seekRelative(self.bufferSeconds)
                    break
                except:
                    if self.currentFileHandle != None:
                        # this can happen if seek() on the file fails
                        _fileMap.closeFile(self.currentFileHandle, self.clientId)
                        self.currentFileHandle = None
                    pass
            time.sleep(1)
        #print "Start point found (currentOffset: " + str(self.currentOffset) + ")"
        #sys.stdout.flush()
        

    def recover(self):
        """
        Recover from EyeTV crashing (or otherwise quitting).
        """
        print "Trying to recover from EyeTV quitting"
        sys.stdout.flush()
        self.clockOffset += 60*90000
            # this number should be about 30 seconds higher than the actual
            # recover time; this way, VLC accepts the new timing, and playback
            # continues smoothly
        self.findStartPoint()
        
        
    def cmd_recover(self, params):
        """
        Initiates recovery from EyeTV crashing.
        """
        global _recoverCount
        
        print "cmd_recover"
        sys.stdout.flush()
        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_recover")
        _recoverCount += 1
        _fileMap.reset(_archivePath)
        _fileMap.waitForRecovery()
        #_fileMap.scan(_archivePath)
        _lock.release("MyHTTPRequestHandlerCommon.cmd_recover")


    def cmd_getChannelList(self, params):
        """
        Retrieves a list of all available channels (including the current
        channel mentioned at the end).

        The format of the returned list is:

            <no>: <name>
            ...
            <no>: <name>
            <no>            (number of current channel)
            <1 or 0>        (recording state)
            <no>            (client id - used to identify a client later on,
                             for example for skipping inside the stream)
            <quality>       (quality of live TV: "SQ", "HQ", "M2", or "M4")

        or (if no auto tune log is found or another error occurs):

            <1 or 0>        (recording state)
            <no>            (client id - used to identify a client later on,
                             for example for skipping inside the stream)
            <quality>       (quality of live TV: "SQ", "HQ", "M2", or "M4")

        Which format is sent can easily be determined by checking if
        the first returned line contains a colon. It is guaranteed that
        the returned data ends with a newline (\n).
        """
        channelNames = self.getChannels(_archivePath)
        for channelName in channelNames:
            self.wfile.write(channelName.encode("UTF-8") + "\n")
        _fileMap.scan(_archivePath)  # make sure we can tell whether EyeTV is recording
        self.wfile.write(str(_fileMap.isEyeTVRecording()) + "\n")
        self.writeCommandOutput("uuidgen")
        self.wfile.write(_fileMap.getCurrentQuality() + "\n")
        #print "channel search finished"
        

    def writeCommandOutput(self, command):
        """
        Writes the output of a shell command to the client stream.
        """
        self.wfile.write(getCommandOutput(command))
        

    def writeCurrentChannel(self):
        """
        Writes the number of the current channel (followed by a newline)
        to the client stream.
        """
        command = "osascript -e 'tell application \"EyeTV\"' -e 'set retVal to current channel'"
        command += " -e 'end tell' -e 'return retVal'"
        self.writeCommandOutput(command)
        
        
    def internalSetChannel(self, newChannel):
        _fileMap.scan(_archivePath)
        if _fileMap.isEyeTVRecording():
            return 0
        else:
            _manageEyeTVLock.acquire("MyHTTPRequestHandlerCommon.internalSetChannel")
            manageEyeTV = _manageEyeTV
            playbackVolume = _playbackVolume
            _manageEyeTVLock.release("MyHTTPRequestHandlerCommon.internalSetChannel")
            command = "set current channel to " + newChannel
            if manageEyeTV:
                command = APPLESCRIPT_PAUSE_EYETV_1 + command + \
                    APPLESCRIPT_PAUSE_EYETV_2 + playbackVolume + \
                    APPLESCRIPT_PAUSE_EYETV_3
            else:
                command = "tell application \"EyeTV\" to " + command
            command = "osascript -e '" + command + "'"
            os.system(command)
            return 1


    def cmd_setChannel(self, params):
        """
        Changes the current channel. On return, two lines are written: one
        containing the actual current channel number and one indicating the
        recording status (0 or 1).
        """
        retVal = self.internalSetChannel(params[0])
        if retVal == 0:
            self.writeCurrentChannel()
            self.wfile.write("1\n")
        else:
            self.wfile.write(params[0] + "\n")
            self.wfile.write("0\n")


    def cmd_channelUp(self, params):
        """
        Switches to the next channel. On return, two lines are written: one
        containing the current channel number and one indicating the recording
        status (0 or 1).
        """
        _fileMap.scan(_archivePath)
        if _fileMap.isEyeTVRecording():
            self.writeCurrentChannel()
            self.wfile.write("1\n")
        else:
            _manageEyeTVLock.acquire("MyHTTPRequestHandlerCommon.cmd_channelUp")
            manageEyeTV = _manageEyeTV
            playbackVolume = _playbackVolume
            _manageEyeTVLock.release("MyHTTPRequestHandlerCommon.cmd_channelUp")
            command = "channel_up"
            if manageEyeTV:
                command = APPLESCRIPT_PAUSE_EYETV_1 + command + \
                    APPLESCRIPT_PAUSE_EYETV_2 + playbackVolume + """
                    return the current channel""" + \
                    APPLESCRIPT_PAUSE_EYETV_3
            else:
                command = """tell application "EyeTV"
                    """ + command + """
                    return the current channel
                    end tell"""
            command = "osascript -e '" + command + "'"
            self.writeCommandOutput(command)
            self.wfile.write("0\n")
    

    def cmd_channelDown(self, params):
        """
        Switches to the previous channel. On return, two lines are written:
        one containing the current channel number and one indicating the
        recording status (0 or 1).
        """
        _fileMap.scan(_archivePath)
        if _fileMap.isEyeTVRecording():
            self.writeCurrentChannel()
            self.wfile.write("1\n")
        else:
            _manageEyeTVLock.acquire("MyHTTPRequestHandlerCommon.cmd_channelDown")
            manageEyeTV = _manageEyeTV
            playbackVolume = _playbackVolume
            _manageEyeTVLock.release("MyHTTPRequestHandlerCommon.cmd_channelDown")
            command = "channel_down"
            if manageEyeTV:
                command = APPLESCRIPT_PAUSE_EYETV_1 + command + \
                    APPLESCRIPT_PAUSE_EYETV_2 + playbackVolume + """
                    return the current channel""" + \
                    APPLESCRIPT_PAUSE_EYETV_3
            else:
                command = """tell application "EyeTV"
                    """ + command + """
                    return the current channel
                    end tell"""
            command = "osascript -e '" + command + "'"
            self.writeCommandOutput(command)
            self.wfile.write("0\n")

    
    def cmd_startRecording(self, params):
        """
        Start recording.
        """
        _manageEyeTVLock.acquire("MyHTTPRequestHandlerCommon.cmd_startRecording")
        manageEyeTV = _manageEyeTV
        playbackVolume = _playbackVolume
        _manageEyeTVLock.release("MyHTTPRequestHandlerCommon.cmd_startRecording")
        command = "start recording"
        if manageEyeTV:
            command = APPLESCRIPT_PAUSE_EYETV_1 + command + \
                APPLESCRIPT_PAUSE_EYETV_2 + playbackVolume + """
                return the current channel""" + \
                APPLESCRIPT_PAUSE_EYETV_3
        else:
            command = """tell application "EyeTV"
                """ + command + """
                return the current channel
                end tell"""
        command = "osascript -e '" + command + "'"
        self.writeCommandOutput(command)
        #self.wfile.write(str(isEyeTVRecording()) + "\n")
        self.wfile.write(str(1) + "\n")
        # don't ask if EyeTV is recording - may not be reflected on disk yet


    def cmd_stopRecording(self, params):
        """
        Stop recording.
        """
        _manageEyeTVLock.acquire("MyHTTPRequestHandlerCommon.cmd_stopRecording")
        manageEyeTV = _manageEyeTV
        playbackVolume = _playbackVolume
        _manageEyeTVLock.release("MyHTTPRequestHandlerCommon.cmd_stopRecording")
        command = "stop recording"
        if manageEyeTV:
            command = APPLESCRIPT_PAUSE_EYETV_1 + command + \
                APPLESCRIPT_PAUSE_EYETV_2 + playbackVolume + """
                return the current channel""" + \
                APPLESCRIPT_PAUSE_EYETV_3
        else:
            command = """tell application "EyeTV"
                """ + command + """
                return the current channel
                end tell"""
        command = "osascript -e '" + command + "'"
        self.writeCommandOutput(command)
        #self.wfile.write(str(isEyeTVRecording()) + "\n")
        self.wfile.write(str(0) + "\n")
        # don't ask if EyeTV is recording - may not be reflected on disk yet


    def cmd_skip(self, params):
        """
        Skips forwards or backwards.
        """
        skipTime = int(params[0])
        clientId = params[1][9:]
        #print "skipTime:", skipTime
        #print "clientId:", clientId
        #sys.stdout.flush()
        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_skip")
        try:
            client = _connectedClients[clientId]
        except:
            client = None
        if client != None:
            if client.safeForChanging:
                client.skipTime = 0
                try:
                    client.seekRelative(skipTime)
                    client.currentFileGuarded = client.currentFile
                    client.currentOffsetGuarded = client.currentOffset
                except:
                    #pass
                    raise
            else:
                client.skipTime = skipTime
        _lock.release("MyHTTPRequestHandlerCommon.cmd_skip")


    def cmd_jumpToPosition(self, params):
        """
        Jumps to a certain position in a stream.
        """
        streamId = params[0]
        streamPos = long(params[1])
        clientId = params[2][9:]
        #print "streamId:", streamId
        #print "streamPos:", streamPos
        #print "clientId:", clientId
        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_jumpToPosition")
        try:
            client = _connectedClients[clientId]
        except:
            client = None
        if client != None:
            if client.safeForChanging:
                client.jumpToStream = None
                client.jumpToStreamPos = None
                client.seekAbsolute(streamId, streamPos)
                client.currentFileGuarded = client.currentFile
                client.currentOffsetGuarded = client.currentOffset
            else:
                client.jumpToStream = streamId
                client.jumpToStreamPos = streamPos
        _lock.release("MyHTTPRequestHandlerCommon.cmd_jumpToPosition")

    
    def cmd_jumpToLive(self, params):
        """
        Jumps to the live tv stream (i.e. to the end of the last stream that
        is currently present).
        """
        clientId = params[0][9:]
        (streamId, streamPos, actualOffset) = _fileMap.seek(_fileMap.SEEK_END, 0)
        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_jumpToLive")
        try:
            client = _connectedClients[clientId]
        except:
            client = None
        if client != None:
            if client.safeForChanging:
                client.jumpToStream = None
                client.jumpToStreamPos = None
                client.seekAbsolute(streamId, streamPos)
                client.currentFileGuarded = client.currentFile
                client.currentOffsetGuarded = client.currentOffset
            else:
                client.jumpToStream = streamId
                client.jumpToStreamPos = streamPos
        _lock.release("MyHTTPRequestHandlerCommon.cmd_jumpToLive")


    def cmd_cacheEmpty(self, params):
        """
        Notification that the cache is empty.
        """
        #print "cache empty"
        #sys.stdout.flush()

        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_cacheEmpty")
        try:
            clientIdIndex = params[0].index("clientId=")
            clientId = params[0][clientIdIndex + 9:]
            client = _connectedClients[clientId]
            #print "cache empty - client found"
            #sys.stdout.flush()
        except:
            client = None
        if client != None:
            client.cacheEmpty = 1   # can safely be set without locking
        _lock.release("MyHTTPRequestHandlerCommon.cmd_cacheEmpty")

        
    def cmd_scanFiles(self, params):
        """
        Scan all available video files.
        """
        _fileMap.scan(_archivePath)


    def cmd_getInfo(self, params):
        """
        Returns information about the video files present on the server. If a
        client id is specified, the file currently played and the position
        within this file are also returned. Finally, the current channel is
        returned.
        """
        #global _infoCounter
        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_getInfo")
        #_infoCounter += 1
        clientId = None
        try:
            clientId = params[0][9:]
            _fileMap.writeInfo(self.wfile, clientId)
            try:
                client = _connectedClients[clientId]
                self.wfile.write(clientId + ":" + str(client.currentFileGuarded) + "," + str(client.currentOffsetGuarded) + "\n")
                #self.writeCurrentChannel()     # too stressful to do every time
            except:
                pass    # never mind (client may not be present in the global list)
        except:
            # in case is doesn't work, make sure the client gets the full list next time
            if clientId is not None:
                _fileMap.resetClient(clientId)
        _lock.release("MyHTTPRequestHandlerCommon.cmd_getInfo")

        
    def cmd_protect(self, params):
        """
        Protects/unprotects future live streams for this client.
        """
        protect = int(params[0])
        clientId = params[1][9:]
        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_protect")
        print "cmd_protect"
        sys.stdout.flush()
        try:
            client = _connectedClients[clientId]
        except:
            client = None
        if client != None:
            if client.safeForChanging:
                if (protect and not client.protect) or (client.protect and not protect):
                    if client.currentFileHandle != None:
                        _fileMap.protectFile(client.currentFileHandle, client.clientId, protect)
                    client.protect = protect
                    client.protectGuarded = protect
            else:
                client.protectGuarded = protect
        _lock.release("MyHTTPRequestHandlerCommon.cmd_protect")


    def cmd_getCurrentChannel(self, params):
        """
        Returns the current channel and the recording state.
        """
        _fileMap.scan(_archivePath)
        self.writeCurrentChannel()
        if _fileMap.isEyeTVRecording():
            self.wfile.write("1\n")
        else:
            self.wfile.write("0\n")
        

    def cmd_pause(self, params):
        """
        Enters or leaves pause mode.
        """
        pause = int(params[0])
        clientId = params[1][9:]
        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_pause")
        try:
            client = _connectedClients[clientId]
        except:
            client = None
        if client != None:
            client.pauseGuarded = pause
        _lock.release("MyHTTPRequestHandlerCommon.cmd_pause")


    def cmd_shutdown(self, params):
        """
        Shuts the server down.
        """
        global _exit
        _lock.acquire("MyHTTPRequestHandlerCommon.cmd_shutdown")
        _exit = 1
        _lock.release("MyHTTPRequestHandlerCommon.cmd_shutdown")


    def cmd_manageEyeTV(self, params):
        """
        Changes the mode specifying whether CyTV should manage EyeTV.
        """
        global _manageEyeTV
        global _closeTVWindowWhenInactive
        self.wfile.write("ack\n")
        ok = 0
        _manageEyeTVLock.acquire("MyHTTPRequestHandlerCommon.cmd_manageEyeTV")
        try:
            _manageEyeTV = int(params[0])
            _closeTVWindowWhenInactive = int(params[1])
            #print "cmd_manageEyeTV", _manageEyeTV, _closeTVWindowWhenInactive
            #sys.stdout.flush()
            if _manageEyeTV:
                fetchPlaybackVolume()
            manageEyeTV = _manageEyeTV
            playbackVolume = _playbackVolume
            ok = 1
        finally:
            _manageEyeTVLock.release("MyHTTPRequestHandlerCommon.cmd_manageEyeTV")
        if ok:
            if manageEyeTV:
                hideEyeTV()
                pauseEyeTV("play", playbackVolume)


    def cmd_eyeTVRestarted(self, params):
        """
        Performs EyeTV management. Call this if EyeTV has been restarted.
        """
        ok = 0
        _manageEyeTVLock.acquire("MyHTTPRequestHandlerCommon.cmd_eyeTVRestarted")
        try:
            if _manageEyeTV:
                fetchPlaybackVolume()
            manageEyeTV = _manageEyeTV
            playbackVolume = _playbackVolume
            ok = 1
        finally:
            _manageEyeTVLock.release("MyHTTPRequestHandlerCommon.cmd_eyeTVRestarted")
        if ok:
            if manageEyeTV:
                hideEyeTV()
                pauseEyeTV("play", playbackVolume)

    
    def cmd_wait(self, params):
        time.sleep(20)


    def startTranscoder(self):
        """
        Start the transcoder if transcode options are present.
        """
        if self.transcodeOptions != None:
            aspectRatio = 1.3333
            try:
                aspectRatio = _fileMap.getAspectRatio(self.currentFile)
            except:
                pass
            #command = "ffmpeg -i - -v 0 -aspect " + str(round(aspectRatio, 4)) + " " + \
            command = "ffmpeg -i - -v 0 " + \
                    self.transcodeOptions + " -" # 2>/dev/null"
            head, tail = os.path.split(sys.argv[0])
            if head != "":
                prefix = "'" + head + "'/"
            else:
                prefix = ""
            #command = prefix + "RealtimeWrapper " + prefix + command
            command = prefix + command
            print command
            sys.stdout.flush()
            self.transcoder = popen2.Popen3(command, 1, TRANSCODER_BLOCKSIZE)
            writerThread = threading.Thread(target=Writer(self))
            writerThread.start()
            errLoggerThread = threading.Thread(target=ErrLogger(self))
            errLoggerThread.start()


    def killTranscoder(self):
        """
        If there is a transcoder process, kill it.
        """
        try:
            if self.transcoder != None:
                try:
                    self.transcoder.tochild.close()
                except:
                    pass
                #os.kill(self.transcoder.pid, signal.SIGTERM)
                os.kill(self.transcoder.pid, signal.SIGKILL)
                self.transcoder.wait()
                #print "Transcoder killed"
                #sys.stdout.flush()
                self.transcoder = None
        except:
            print "Cannot kill transcoder"
            sys.stdout.flush()


    def handleError(self, exc_info, tryToRecover):
        """
        Error handling in case reading or writing fails.
        Returns 1 if the error was handled or 0 if no recovery
        is possible.
        """
        if self.currentFileHandle != None:
            _fileMap.closeFile(self.currentFileHandle, self.clientId)
            self.currentFileHandle = None
        if tryToRecover:
            _lock.acquire("MyHTTPRequestHandlerCommon.handleError 1")
            recoverCount = _recoverCount
            _lock.release("MyHTTPRequestHandlerCommon.handleError 1")
            if recoverCount != self.recoverCount:
                self.recovering = 1
                self.recover()
                self.recoverCount = recoverCount
                self.recovering = 0
                return 1
        self.killTranscoder()
        #print "sys.exc_info()[0]:", str(sys.exc_info()[0])
        #sys.stdout.flush()
        #-> socket.error
        if exc_info[0] == IOError or exc_info[0] == socket.error:
            if self.clientId != None:
                _lock.acquire("MyHTTPRequestHandlerCommon.handleError 2")
                try:
                    clientsCount = _connectedClientsCount[self.clientId]-1
                    if clientsCount > 0:
                        _connectedClientsCount[self.clientId] = clientsCount
                    else:
                        del _connectedClients[self.clientId]
                        del _connectedClientsCount[self.clientId]
                        _fileMap.removeClient(self.clientId)
                except:
                    pass
                _lock.release("MyHTTPRequestHandlerCommon.handleError 2")
                print "Client disconnected with id " + self.clientId
            else:
                print "Client disconnected"
            sys.stdout.flush()
            self.close_connection = 1
            return 0
        else:
            raise


    def do_GET_p(self):
        profiler = hotshot.Profile("/Users/andreasj/Library/Logs/cytv_profile.log")
        profiler.runcall(self.do_GET_p)
        #self.do_GET_p()


    def do_GET(self):
        # TODO: rewrite parameter parsing/command processing
        _lock.acquire("MyHTTPRequestHandlerCommon.do_GET 1")
        self.recoverCount = _recoverCount
        _lock.release("MyHTTPRequestHandlerCommon.do_GET 1")
        self.jumpToStream = None
        self.jumpToStreamPos = None
        self.skipTime = 0
        self.recovering = 0
        self.currentFile = None
        self.currentOffset = 0
        self.currentFileHandle = None
        self.clientId = None
        self.protect = 0
        self.pause = 0
        self.safeForChanging = 0
        self.transcodeOptions = None
        self.videoBitrate = 2300    # anything but 0 is OK as the default
        self.audioBitrate = 224     # dito
        self.writerThreadError = None
        self.streamingCacheClient = 0
        self.cacheEmpty = 0
        self.initialDelayInserted = 0
        self.cacheSize = 0
        self.bufferBytes = 0        # buffer we're staying away from
        self.bufferSeconds = 0      # the end (to prevent choppy playback)
        try:
            if self.headers["X-cytv-cache"] == "stream":
                self.streamingCacheClient = 1
        except:
            pass
        self.bytesWritten = 0
        if len(self.path) > 1:
            if self.path[1:5] == "test":
                handle = open("/Users/andreasj/Desktop/mpeg4sample.ts", "rb")
                self.wfile.write("HTTP/1.1 200 OK\r\n")
                self.send_header("Connection", "close")
                self.send_header("Content-Type", "video/mpeg")
                self.end_headers()
                self.wfile.flush()
                while 1:
                    bytes = handle.read(188)
                    if len(bytes) < 188:
                        return
                    self.wfile.write(bytes)
            if self.path[1:5] == "html":
                # HTML mode
                self.wfile.write("HTTP/1.1 200 OK\r\n")
                self.send_header("Connection", "close")
                self.send_header("Content-Type", "text/html;charset=utf-8")
                index = HTML_OUTPUT.index("$channels")
                channelList = self.getChannels(_archivePath)
                htmlChannelList = ""
                if len(channelList) > 0:
                    currentChannel = channelList[-1].rjust(3)
                    for channel in channelList[:-1]:
                        if currentChannel == channel[:3]:
                            htmlChannelList += "<option selected='selected'>" + htmlEscape(channel.encode("UTF-8")) + "</option>"
                        else:
                            htmlChannelList += "<option>" + htmlEscape(channel.encode("UTF-8")) + "</option>"
                self.send_header("Content-Length", str(len(HTML_OUTPUT) - 9 + len(htmlChannelList)))
                self.end_headers()
                self.wfile.write(HTML_OUTPUT[:index])
                self.wfile.write(htmlChannelList)
                self.wfile.write(HTML_OUTPUT[index+9:])
                #print "html client: " + self.path
                #sys.stdout.flush()
                return
            elif self.path[1:11] == "?clientId=":
                # parse the parameters
                paramlist = urllib.unquote_plus(self.path[2:]).split("&")
                params = {}
                for param in paramlist:
                    paramSplitted = param.split("=")
                    if len(paramSplitted) == 2:
                        params[paramSplitted[0]] = paramSplitted[1]
                #print self.path
                #print params
                #sys.stdout.flush()
                if params.has_key("clientId"):
                    self.clientId = params["clientId"]
                if params.has_key("protect"):
                    self.protect = int(params["protect"])
                if params.has_key("streamId"):
                    self.jumpToStream = params["streamId"]
                    self.jumpToStreamPos = 0
                if params.has_key("transcode"):
                    if params["transcode"].upper() == "TRUE":
                        # read transcoding properties
                        self.transcodeOptions = "-vcodec h263p -acodec mp2 -f avi" # -hq"
                        if params.has_key("videoBitrate"):
                            self.videoBitrate = int(params["videoBitrate"])
                            self.transcodeOptions += " -b " + str(self.videoBitrate)
                        if params.has_key("framerate"):
                            self.transcodeOptions += " -r " + str(float(params["framerate"]))
                        if params.has_key("width") and params.has_key("height"):
                            self.transcodeOptions += " -s " + str(int(params["width"])) + "x" + \
                                                              str(int(params["height"]))
                        if params.has_key("audioBitrate"):
                            self.audioBitrate = int(params["audioBitrate"])
                            self.transcodeOptions += " -ab " + str(self.audioBitrate)
                        if params.has_key("sampleRate"):
                            self.transcodeOptions += " -ar " + str(int(params["sampleRate"]))
                        if params.has_key("numberOfChannels"):
                            self.transcodeOptions += " -ac " + str(int(params["numberOfChannels"]))
                        if params.has_key("deinterlace"):
                            if params["deinterlace"].upper() == "TRUE":
                                self.transcodeOptions += " -deinterlace"
                        if params.has_key("highQuality"):
                            if params["highQuality"].upper() == "TRUE":
                                self.transcodeOptions += " -hq"
                if params.has_key("cacheSize"):
                    self.cacheSize = int(params["cacheSize"])
                    print "cache size set to %d KB" % self.cacheSize
                    sys.stdout.flush()
                self.currentFileGuarded = None
                self.currentOffsetGuarded = None
                self.protectGuarded = self.protect
                self.pauseGuarded = 0
                if params.has_key("channel"):
                    self.internalSetChannel(params["channel"])
            else:
                # command execution mode
                if self.path[1] == '?':
                    #print self.path[2:]
                    params = self.path[2:].split('&')
                    params = [urllib.unquote_plus(param) for param in params]
                    if len(params) > 0:
                        command = params[0]
                        error = 0
                        try:
                            command_method = getattr(self, "cmd_" + command)
                        except:
                            error = 1
                        if not error:
                            try:
                                self.wfile.write("HTTP/1.1 200 OK\r\n")
                                self.send_header("Connection", "close")
                                self.send_header("Content-Type", "text/plain")
                                self.end_headers()
                            except:
                                error = 1
                            if not error:
                                command_method(params[1:])
                            return
                self.send_error(404)
                return
        else:
            self.currentFileGuarded = None
            self.currentOffsetGuarded = None
            self.protectGuarded = self.protect
            self.pauseGuarded = 0
        
        #print self.path
        #sys.stdout.flush()
        self.clockOffset = 0L
        self.headerWritten = 0
        self.transcoder = None

        # variables for on-the-fly insertion of PCR packets
        self.generatePCR = 0
        self.pcrId = 0
        self.pcrContinuity = 0
        if self.transcodeOptions is None:
            self.detectPCRState = PCR_STATE_INIT
            self.firstPTS = 0L
        else:
            self.detectPCRState = PCR_STATE_FINISHED

        # put the client into the list of connected clients
        if self.clientId is not None:
            _lock.acquire("MyHTTPRequestHandlerCommon.do_GET 2")
            try:
                clientsCount = _connectedClientsCount[self.clientId]
            except:
                clientsCount = 0
            _connectedClientsCount[self.clientId] = clientsCount + 1
            # allow multiple connections from a single client
            # (VLC sometimes disconnects and reconnects when it starts
            # playing)
            _connectedClients[self.clientId] = self
            _lock.release("MyHTTPRequestHandlerCommon.do_GET 2")
            print "Client connected with id " + self.clientId
            #print self.path
            sys.stdout.flush()
        
        # find the start point and start the transcoder (if requested)
        try:
            self.findStartPoint(self.jumpToStream)
            self.startTranscoder()
        except:
            self.handleError(sys.exc_info(), 0)
            # don't try to recover at this early stage
            return
                
        realtime.boostpriority()
        exit = 0
        while not exit:
            _lock.acquire("MyHTTPRequestHandlerCommon.do_GET 3")
            exit = _exit
            writerThreadError = self.writerThreadError
            jumpToStream = self.jumpToStream
            jumpToStreamPos = self.jumpToStreamPos
            self.jumpToStream = None
            self.jumpToStreamPos = None
            skipTime = self.skipTime
            self.skipTime = 0
            self.currentFileGuarded = self.currentFile
            self.currentOffsetGuarded = self.currentOffset
            protect = self.protectGuarded
            self.pause = self.pauseGuarded
            _lock.release("MyHTTPRequestHandlerCommon.do_GET 3")
            if writerThreadError != None:
                self.handleError(writerThreadError, 0)
                # this will return normally or raise an exception
                # (don't try to recover here, since the
                # transcoder is broken if writerThreadError != None)
                return
            if exit:
                if self.currentFileHandle != None:
                    _fileMap.closeFile(self.currentFileHandle, self.clientId)
                    self.currentFileHandle = None
                self.killTranscoder()
            else:
                if protect != self.protect:
                    if self.currentFileHandle != None:
                        _fileMap.protectFile(self.currentFileHandle, self.clientId, protect)
                    self.protect = protect
                try:
                    if jumpToStream != None:
                        try:
                            self.seekAbsolute(jumpToStream, jumpToStreamPos)
                        except:
                            pass
                    elif skipTime != 0:
                        try:
                            self.seekRelative(skipTime)
                        except:
                            pass
                    if self.pause:
                        time.sleep(1)
                    else:
                        self.readAndWriteStream()
                except:
                    recovered = self.handleError(sys.exc_info(), 1)
                    if not recovered:
                        return


    def getChannels(self, archivePath):
        """
        Returns a list of available channels with the number of the current
        channel at the end.
        """
        if len(_channels) == 0:
            # if we have only the current channel (and no list of names),
            # treat it like there's no channel info at all (this is more
            # consistent and can be handled by the client better)
            return []

        tempname = tempfile.mktemp()
        temphandle = None
        currentChannel = 0
        try:
            command = "osascript -e 'tell application \"EyeTV\" to return the current channel' >" + tempname
            #print command
            os.system(command)
            temphandle = open(tempname, "r")
            currentChannel = int(temphandle.readline())
            temphandle.close()
            temphandle = None
            os.remove(tempname)
        except:
            if temphandle != None:
                try:
                    temphandle.close()
                except:
                    pass
            try:
                os.remove(tempname)
            except:
                pass
            
        return _channels + [str(currentChannel)]

    
class MyHTTPRequestHandler_1_3_1(MyHTTPRequestHandlerCommon):
    """
    Specialized request handler for EyeTV 1.3.1 and lower.
    """
    pass
    
    
def collectChannels_1_3_1(archivePath):
    """
    Collects all available channels.
    Call this once before calling getChannels (an arbitrary number of times).
    """
    global _channels
    global _channelsDict

    _channels = []
    _channelsDict = {}
    
    channels = []
    lastChannel = None
    autoTuneLog = None
    try:
        autoTuneLog = open(os.path.join(archivePath, TUNERLOG_FILE), "r")
        while 1:
            line = autoTuneLog.readline()
            if line != "":
                match = AUTOTUNE_CHANNEL.match(line)
                if match != None:
                    lastChannel = match.group(1)
                else:
                    match = CHANNEL_FOUND.match(line)
                    if match != None and not lastChannel in channels:
                        channels.append(lastChannel)
            else:
                break
        autoTuneLog.close()
        
        tempname = tempfile.mktemp()
        temphandle = None
        try:
            command = "osascript -e 'tell application \"EyeTV\"' -e 'set retVal to \"\"'"
            for channel in channels:
                command += " -e 'if enabled of channel id " + channel + " then'"
                command += " -e 'set retVal to retVal & \"" + channel + ":\" & name of channel id " + channel + " & \"\\n\"'"
                command += " -e 'end if'"
            #command += " -e 'set retVal to retVal & current channel'"
            command += " -e 'return retVal' -e 'end tell' >" + tempname
            #print command
            os.system(command)
            temphandle = open(tempname, "r")
            while 1:
                channelName = temphandle.readline()
                if channelName != "":
                    _channels.append(channelName[:-1])
                else:
                    break
            temphandle.close()
            temphandle = None
            os.remove(tempname)
        except:
            if temphandle != None:
                try:
                    temphandle.close()
                except:
                    pass
            try:
                os.remove(tempname)
            except:
                pass
            raise
        
        #for index in range(len(channels)):
        #    self.wfile.write(channels[index].rjust(3) + ": " + channelNames[index] + "\n")
        for channelIndex in range(len(_channels)):
            channelName = _channels[channelIndex]
            index = channelName.find(":")
            if index != -1:
                _channels[channelIndex] = channelName[:index].rjust(3) + ": " + channelName[index+1:]

    except:
        if autoTuneLog != None:
            try:
                autoTuneLog.close()
            except:
                pass
        #raise

    
class MyHTTPRequestHandler_1_3_2(MyHTTPRequestHandlerCommon):
    """
    Specialized request handler for EyeTV 1.3.2 and higher.
    """
    pass

    
def collectChannels_1_3_2(archivePath):
    """
    Collects a list af all available channels.

    FIXME: The way the preferences are parsed here is a hack; proper XML
    parsing should be used instead of fragile regexes, and unescaping
    should be performed properly instead of using the (rather primitive)
    unescapeXML() function.
    """
    global _channels
    global _channelsDict

    _channels = []
    _channelsDict = {}
    try:
        plistdict = plist.readplist(GLOBAL_PREFS_FILE)
        activeheadends = [x["headend"] for x in plistdict["active headends"]]
        headendkeys = plistdict["headend channel lists"].keys()
        headendkeys.sort()
        for headendkey in headendkeys:
            if hexIdToString(headendkey) in activeheadends:
                for channelDict in plistdict["headend channel lists"][headendkey]:
                    if channelDict["active"]:
                        if _useAltNumbering:
                            channel = str(channelDict["channel number"]).rjust(3) + ": "
                        else:
                            channel = str(len(_channels)+1).rjust(3) + ": "
                        try:
                            channel += channelDict["channel name"]
                        except:
                            pass
                        _channels.append(channel)
                        try:
                            _channelsDict[channelDict["service"]] = channel
                        except:
                            pass
    except:
        pass

    
class Scanner(threading.Thread):
    """
    Background thread scanning for new video files.
    """
    
    def run(self):
        while 1:
            time.sleep(1)   # look for new files every second
            _lock.acquire("Scanner.run")
            localExit = _exit
            _lock.release("Scanner.run")
            if localExit:
                break
            _fileMap.scan(_archivePath)
            sys.stdout.flush()


if __name__ == "__main__":
    
    #gc.set_debug(gc.DEBUG_LEAK + gc.DEBUG_STATS)
    print "CyTV Server started at " + time.strftime("%Y-%m-%d %H:%M:%S")
    print "=========================================="
    print
    #if os.path.exists(VERSION_DETECT_FILE):
    #    _eyeTVVersion = EYETV_VERSION_1_3_2
    #    print "EyeTV version 1.3.2 or greater detected"
    #    MyHTTPRequestHandler = MyHTTPRequestHandler_1_3_2
    #    collectChannels = collectChannels_1_3_2
    #else:
    #    _eyeTVVersion = EYETV_VERSION_1_3_1
    #    print "EyeTV version 1.3.1 or lower detected"
    #    MyHTTPRequestHandler = MyHTTPRequestHandler_1_3_1
    #    collectChannels = collectChannels_1_3_1
    #print
    sys.stdout.flush()
    
    _lock = LoggingLock("~/Library/Logs/CyTV/global_lock.log")
    _manageEyeTVLock = LoggingLock("~/Library/Logs/CyTV/manageeyetv_lock.log")
    _archivePath = "."
    waitForEyeTV = 0
    _eyeTVVersion = EYETV_VERSION_1_3_2
    try:
        _archivePath = sys.argv[2]
        _manageEyeTV = int(sys.argv[3])
        _closeTVWindowWhenInactive = int(sys.argv[4])
        _useAltNumbering = int(sys.argv[5])
        waitForEyeTV = int(sys.argv[6])
        version = sys.argv[7]
        if version < "1.3.2":
            _eyeTVVersion = EYETV_VERSION_1_3_1
        #print "settings:", _manageEyeTV, _closeTVWindowWhenInactive, _useAltNumbering, waitForEyeTV
        #sys.stdout.flush();
    except:
        pass

    if _eyeTVVersion == EYETV_VERSION_1_3_2:
        MyHTTPRequestHandler = MyHTTPRequestHandler_1_3_2
        collectChannels = collectChannels_1_3_2
    else:
        MyHTTPRequestHandler = MyHTTPRequestHandler_1_3_1
        collectChannels = collectChannels_1_3_1

    collectChannels(_archivePath)
    _fileMap = FileMap()
    startTime = time.time()
    _fileMap.scan(_archivePath)
    endTime = time.time()
    print "Initial scan completed in " + str(endTime-startTime)
    sys.stdout.flush()

    if waitForEyeTV:
        lastChangeTime = _fileMap.lastChangeTime
        while 1:
            print "waiting for EyeTV (sleeping 1 sec)"
            time.sleep(1)
            _fileMap.scan(_archivePath)
            if _fileMap.lastChangeTime != lastChangeTime:
                break
            lastChangeTime = _fileMap.lastChangeTime

    try:
        if _manageEyeTV:
            fetchPlaybackVolume()
            hideEyeTV()
            pauseEyeTV("play", _playbackVolume)
    except:
        pass

    scanner = Scanner()
    #scanner.setDaemon(1)
    scanner.start()
    try:
        SimpleHTTPServer.test(MyHTTPRequestHandler, MyHTTPServer)
    except KeyboardInterrupt:
        _lock.acquire("__main__")
        _exit = 1
        _lock.release("__main__")
    print "Server stopped"
    print
    print
    sys.stdout.flush()
    #outstream.close()

