fix threading bugs, add further HTML controls
[ppt-control.git] / obs_ppt_server.py
index 221179e9867ae8d58fedb60fae80783c38db6786..4172d9493d8ccf92fe26004e752a6efa2863ab7e 100755 (executable)
@@ -1,3 +1,5 @@
+import sys\r
+sys.coinit_flags= 0\r
 import win32com.client\r
 import pywintypes\r
 import os\r
@@ -9,26 +11,64 @@ import threading
 import asyncio\r
 import websockets\r
 import logging, json\r
+import urllib\r
+import posixpath\r
+import time\r
+import pythoncom\r
 \r
 logging.basicConfig()\r
 \r
-powerpoint = None\r
-cache = r'''C:\Windows\Temp'''\r
+global current_slideshow\r
+current_slideshow = None\r
+CACHEDIR = r'''C:\Windows\Temp\ppt-cache'''\r
+CACHE_TIMEOUT = 2*60*60\r
 \r
-class Handler(server.CGIHTTPRequestHandler):\r
+class Handler(server.SimpleHTTPRequestHandler):\r
     def __init__(self, *args, **kwargs):\r
         super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__)))\r
+        \r
+    def translate_path(self, path):\r
+        """Translate a /-separated PATH to the local filename syntax.\r
+\r
+        Components that mean special things to the local file system\r
+        (e.g. drive or directory names) are ignored.  (XXX They should\r
+        probably be diagnosed.)\r
+\r
+        """\r
+        # abandon query parameters\r
+        path = path.split('?',1)[0]\r
+        path = path.split('#',1)[0]\r
+        # Don't forget explicit trailing slash when normalizing. Issue17324\r
+        trailing_slash = path.rstrip().endswith('/')\r
+        try:\r
+            path = urllib.parse.unquote(path, errors='surrogatepass')\r
+        except UnicodeDecodeError:\r
+            path = urllib.parse.unquote(path)\r
+        path = posixpath.normpath(path)\r
+        words = path.split('/')\r
+        words = list(filter(None, words))\r
+        if len(words) > 0 and words[0] == "cache":\r
+            if current_slideshow:\r
+                path = CACHEDIR + "\\" + current_slideshow.name()\r
+            words.pop(0)\r
+        else:\r
+            path = self.directory\r
+        for word in words:\r
+            if os.path.dirname(word) or word in (os.curdir, os.pardir):\r
+                # Ignore components that are not a simple file/directory name\r
+                continue\r
+            path = os.path.join(path, word)\r
+        if trailing_slash:\r
+            path += '/'\r
+        print(path)\r
+        return path\r
+\r
 \r
 def run_http():\r
     http_server = server.HTTPServer(("", 80), Handler)\r
     http_server.serve_forever()\r
 \r
-async def first(websocket, path):\r
-    slideshow_view_first()\r
-    await websocket.send(True)\r
-\r
-STATE = {"value": "?", "visible": "1"}\r
-\r
+STATE = {"connected": 0, "current": 0, "total": 0, "visible": 0}\r
 USERS = set()\r
 \r
 \r
@@ -41,7 +81,11 @@ def users_event():
 \r
 \r
 async def notify_state():\r
-    STATE["value"] = str(current_slide()) + "/" + str(total_slides())\r
+    global current_slideshow\r
+    if current_slideshow:\r
+        STATE["current"] = current_slideshow.current_slide()\r
+        STATE["total"] = current_slideshow.total_slides()\r
+        STATE["visible"] = current_slideshow.visible()\r
     if USERS:  # asyncio.wait doesn't accept an empty list\r
         message = state_event()\r
         await asyncio.wait([user.send(message) for user in USERS])\r
@@ -64,6 +108,7 @@ async def unregister(websocket):
 \r
 \r
 async def ws_handle(websocket, path):\r
+    global current_slideshow\r
     # register(websocket) sends user_event() to websocket\r
     await register(websocket)\r
     try:\r
@@ -71,16 +116,43 @@ async def ws_handle(websocket, path):
         async for message in websocket:\r
             data = json.loads(message)\r
             if data["action"] == "prev":\r
-                slideshow_view_previous()\r
+                if current_slideshow:\r
+                    current_slideshow.prev()\r
                 await notify_state()\r
             elif data["action"] == "next":\r
-                slideshow_view_next()\r
+                if current_slideshow:\r
+                    current_slideshow.next()\r
                 await notify_state()\r
             elif data["action"] == "first":\r
-                slideshow_view_first()\r
+                if current_slideshow:\r
+                    current_slideshow.first()\r
                 await notify_state()\r
             elif data["action"] == "last":\r
-                slideshow_view_last()\r
+                if current_slideshow:\r
+                    current_slideshow.last()\r
+                await notify_state()\r
+            elif data["action"] == "black":\r
+                if current_slideshow:\r
+                    if current_slideshow.visible() == 3:\r
+                        current_slideshow.normal()\r
+                    else:\r
+                        current_slideshow.black()\r
+                await notify_state()\r
+            elif data["action"] == "white":\r
+                if current_slideshow:\r
+                    if current_slideshow.visible() == 4:\r
+                        current_slideshow.normal()\r
+                    else:\r
+                        current_slideshow.white()\r
+                await notify_state()\r
+            elif data["action"] == "goto":\r
+                if current_slideshow:\r
+                    current_slideshow.goto(int(data["value"]))\r
+                await notify_state()\r
+            elif data["action"] == "refresh":\r
+                if current_slideshow:\r
+                    current_slideshow.export_current_next()\r
+                    current_slideshow.refresh()\r
                 await notify_state()\r
             else:\r
                 logging.error("unsupported event: {}", data)\r
@@ -88,20 +160,27 @@ async def ws_handle(websocket, path):
         await unregister(websocket)\r
 \r
 def run_ws():\r
+    pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED)\r
+    asyncio.set_event_loop(asyncio.new_event_loop())\r
     start_server = websockets.serve(ws_handle, "0.0.0.0", 5678)\r
+    print("Initialised websocket server")\r
     asyncio.get_event_loop().run_until_complete(start_server)\r
+    print("Running websocket server until complete") \r
     asyncio.get_event_loop().run_forever()\r
 \r
 def start_server():\r
-    STATE["value"] = current_slide()\r
+    #STATE["current"] = current_slide()\r
     http_daemon = threading.Thread(name="http_daemon", target=run_http)\r
     http_daemon.setDaemon(True)\r
     http_daemon.start()\r
+    print("Started HTTP server")\r
 \r
-    run_ws()\r
-    #ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)\r
-    #ws_daemon.setDaemon(True)\r
-    #ws_daemon.start()\r
+    #run_ws()\r
+    \r
+    ws_daemon = threading.Thread(name="ws_daemon", target=run_ws)\r
+    ws_daemon.setDaemon(True)\r
+    ws_daemon.start()\r
+    print("Started websocket server")\r
 \r
     #try:\r
     #    ws_daemon.start()\r
@@ -110,103 +189,138 @@ def start_server():
     #    cleanup_stop_thread()\r
     #    sys.exit()\r
 \r
-\r
-def get_slideshow_view():\r
-    global powerpoint\r
-\r
-    if powerpoint is None:\r
-        powerpoint = win32com.client.Dispatch('Powerpoint.Application')\r
-\r
-    if powerpoint is None:\r
-        return\r
-\r
-    ssw = powerpoint.SlideShowWindows\r
-    if ssw.Count == 0:\r
-        return\r
-\r
-    # https://docs.microsoft.com/en-us/office/vba/api/powerpoint.slideshowwindow.view\r
-    ssv = ssw[0].View\r
-\r
-    return ssv\r
-\r
-def get_activepresentation():\r
-    global powerpoint\r
-\r
-    if powerpoint is None:\r
-        powerpoint = win32com.client.Dispatch('Powerpoint.Application')\r
-\r
-    if powerpoint is None:\r
-        return\r
-\r
-    activepres = powerpoint.ActivePresentation\r
-    return activepres\r
-\r
-def total_slides():\r
-    ssp = get_activepresentation()\r
-    if ssp:\r
-        return len(ssp.Slides)\r
-\r
-def current_slide():\r
-    ssv = get_slideshow_view()\r
-    if ssv:\r
-        return ssv.CurrentShowPosition\r
-\r
-def export(slide):\r
-    global cache\r
-    ssp = get_activepresentation()\r
-    if ssp:\r
-        for (slide, name) in [(slide, "current"), (slide+1, "next")]:\r
-            if slide < len(ssp.Slides):\r
-                ssp.Slides(slide).Export(os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + r'''0.jpg''', "JPG")\r
+class Slideshow:\r
+    def __init__(self, instance):\r
+        self.instance = instance\r
+        if self.instance is None:\r
+            raise ValueError("PPT instance cannot be None")\r
+\r
+        if self.instance.SlideShowWindows.Count == 0:\r
+            raise ValueError("PPT instance has no slideshow windows")\r
+        self.view = self.instance.SlideShowWindows[0].View\r
+\r
+        if self.instance.ActivePresentation is None:\r
+            raise ValueError("PPT instance has no  active presentation")\r
+        self.presentation = self.instance.ActivePresentation\r
+        \r
+        self.export_all()\r
+\r
+    def refresh(self):\r
+        if self.instance is None:\r
+            raise ValueError("PPT instance cannot be None")\r
+\r
+        #if self.instance.SlideShowWindows.Count == 0:\r
+        #    raise ValueError("PPT instance has no slideshow windows")\r
+        self.view = self.instance.SlideShowWindows[0].View\r
+\r
+        if self.instance.ActivePresentation is None:\r
+            raise ValueError("PPT instance has no  active presentation")\r
+\r
+    def total_slides(self):\r
+        return len(self.presentation.Slides)\r
+\r
+    def current_slide(self):\r
+        return self.view.CurrentShowPosition\r
+\r
+    def visible(self):\r
+        return self.view.State\r
+\r
+    def prev(self):\r
+        self.refresh()\r
+        self.view.Previous()\r
+\r
+    def next(self):\r
+        self.refresh()\r
+        self.view.Next()\r
+        self.export_current_next()\r
+\r
+    def first(self):\r
+        self.refresh()\r
+        self.view.First()\r
+        self.export_current_next()\r
+                \r
+    def last(self):\r
+        self.refresh()\r
+        self.view.Last()\r
+        self.export_current_next()\r
+\r
+    def goto(self, slide):\r
+        self.refresh()\r
+        if slide <= self.total_slides():\r
+            self.view.GotoSlide(slide)\r
+        else:\r
+            self.last()\r
+            self.next()\r
+        self.export_current_next()\r
+\r
+    def black(self):\r
+        self.refresh()\r
+        self.view.State = 3\r
+\r
+    def white(self):\r
+        self.refresh()\r
+        self.view.State = 4\r
+\r
+    def normal(self):\r
+        self.refresh()\r
+        self.view.State = 1\r
+\r
+    def name(self):\r
+        return self.presentation.Name\r
+\r
+    def export_current_next(self):\r
+        self.export(self.current_slide())\r
+        self.export(self.current_slide() + 1)\r
+\r
+    def export(self, slide):\r
+        destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg"\r
+        os.makedirs(os.path.dirname(destination), exist_ok=True)\r
+        if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT:\r
+            if slide <= self.total_slides():\r
                 attempts = 0\r
                 while attempts < 3:\r
                     try:\r
-                        os.replace(os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + r'''0.jpg''', os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + '''.jpg''')\r
+                        self.presentation.Slides(slide).Export(destination, "JPG")\r
+                        time.sleep(0.5)\r
+                        break\r
                     except:\r
                         pass\r
                     attempts += 1\r
+            elif slide == self.total_slides() + 1:\r
+                shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb'))\r
             else:\r
-                    shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\blank.jpg''', 'rb'), open(os.path.dirname(os.path.realpath(__file__)) + r'''\\''' + name + r'''next.jpg''', 'wb'))\r
-\r
-def slideshow_view_first():\r
-    ssv = get_slideshow_view()\r
-    if ssv:\r
-        ssv.First()\r
-        export(ssv.CurrentShowPosition)\r
-\r
-def slideshow_view_previous():\r
-    ssv = get_slideshow_view()\r
-    if ssv:\r
-        ssv.Previous()\r
-        export(ssv.CurrentShowPosition)\r
-\r
-def slideshow_view_next():\r
-    ssv = get_slideshow_view()\r
-    if ssv:\r
-        ssv.Next()\r
-        export(ssv.CurrentShowPosition)\r
-            \r
-\r
-def slideshow_view_last():\r
-    ssv = get_slideshow_view()\r
-    if ssv:\r
-        ssv.Last()\r
-        export(ssv.CurrentShowPosition)\r
-\r
-def slideshow_view_black():\r
-    ssv = get_slideshow_view()\r
-    if ssv:\r
-        ssv.State = 3\r
-\r
-def slideshow_view_white():\r
-    ssv = get_slideshow_view()\r
-    if ssv:\r
-        ssv.State = 4\r
-\r
-def slideshow_view_normal():\r
-    ssv = get_slideshow_view()\r
-    if ssv:\r
-        ssv.State = 1\r
+                pass\r
+\r
+    def export_all(self):\r
+        for i in range(1, self.total_slides()):\r
+            self.export(i)\r
+\r
+def get_ppt_instance():\r
+    instance = win32com.client.Dispatch('Powerpoint.Application')\r
+    if instance is None or instance.SlideShowWindows.Count == 0:\r
+        return None\r
+    return instance\r
+\r
+def get_current_slideshow():\r
+    print(str(current_slideshow))\r
+    return current_slideshow\r
+\r
 \r
 if __name__ == "__main__":\r
+\r
     start_server()\r
+    \r
+    while True:\r
+        # Check if PowerPoint is running\r
+        instance = get_ppt_instance()\r
+        try:\r
+            current_slideshow = Slideshow(instance)\r
+            STATE["connected"] = 1\r
+            STATE["current"] = current_slideshow.current_slide()\r
+            STATE["total"] = current_slideshow.total_slides()\r
+            print("Connected to PowerPoint instance " + str(get_current_slideshow()))\r
+            break\r
+        except ValueError as e:\r
+            current_slideshow = None\r
+            pass\r
+        time.sleep(1)\r