Эх сурвалжийг харах

voice messages: convert incoming voice messages to ogg

Sumner Evans 3 жил өмнө
parent
commit
7e5f4c71aa

+ 21 - 4
mautrix_instagram/portal.py

@@ -36,6 +36,8 @@ from mautrix.types import (EventID, MessageEventContent, RoomID, EventType, Mess
 from mautrix.errors import MatrixError, MForbidden, MNotFound, SessionNotFound
 from mautrix.util.simple_lock import SimpleLock
 
+from mautrix_instagram.util.audio_convert import to_ogg
+
 
 from .db import Portal as DBPortal, Message as DBMessage, Reaction as DBReaction
 from .config import Config
@@ -465,10 +467,18 @@ class Portal(DBPortal, BasePortal):
 
     async def _reupload_instagram_voice(self, source: 'u.User', media: VoiceMediaItem,
                                         intent: IntentAPI) -> MediaMessageEventContent:
+        async def convert_to_ogg(data, mimetype):
+            converted = await to_ogg(data, mimetype)
+            if converted is None:
+                raise ChildProcessError("Failed to convert to OGG")
+            return converted, "audio/ogg"
+
         url = media.media.audio.audio_src
         info = AudioInfo(duration=media.media.audio.duration)
         waveform = [int(p * 1000) for p in media.media.audio.waveform_data]
-        content = await self._reupload_instagram_file(source, url, MessageType.AUDIO, info, intent)
+        content = await self._reupload_instagram_file(
+            source, url, MessageType.AUDIO, info, intent, convert_to_ogg
+        )
         content["org.matrix.msc1767.file"] = {
             "url": content.url,
             "name": content.body,
@@ -482,9 +492,10 @@ class Portal(DBPortal, BasePortal):
         content["org.matrix.msc3245.voice"] = {}
         return content
 
-    async def _reupload_instagram_file(self, source: 'u.User', url: str, msgtype: MessageType,
-                                       info: FileInfo, intent: IntentAPI
-                                       ) -> MediaMessageEventContent:
+    async def _reupload_instagram_file(
+        self, source: 'u.User', url: str, msgtype: MessageType, info: FileInfo, intent: IntentAPI,
+        convert_fn: Optional[Callable[[bytes, str], Awaitable[Tuple[bytes, str]]]] = None,
+    ) -> MediaMessageEventContent:
         async with await source.client.raw_http_get(url) as resp:
             try:
                 length = int(resp.headers["Content-Length"])
@@ -499,12 +510,18 @@ class Portal(DBPortal, BasePortal):
                 raise ValueError("Attachment not available: too large")
             data = await resp.read()
             info.mimetype = resp.headers["Content-Type"] or magic.from_buffer(data, mime=True)
+
+        # Run the conversion function on the data.
+        if convert_fn is not None:
+            data, info.mimetype = await convert_fn(data, info.mimetype)
+
         info.size = len(data)
         extension = {
             "image/webp": ".webp",
             "image/jpeg": ".jpg",
             "video/mp4": ".mp4",
             "audio/mp4": ".m4a",
+            "audio/ogg": ".ogg",
         }.get(info.mimetype)
         extension = extension or mimetypes.guess_extension(info.mimetype) or ""
         file_name = f"{msgtype.value[2:]}{extension}"

+ 63 - 0
mautrix_instagram/util/audio_convert.py

@@ -0,0 +1,63 @@
+# mautrix-instagram - A Matrix-Instagram puppeting bridge.
+# Copyright (C) 2021 Tulir Asokan, Sumner Evans
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+from typing import Optional
+import mimetypes
+import tempfile
+import logging
+import asyncio
+import shutil
+import os
+
+log = logging.getLogger("mau.util.audio")
+
+
+def abswhich(program: Optional[str]) -> Optional[str]:
+    if program is None:
+        return None
+    path = shutil.which(program)
+    return os.path.abspath(path) if path else None
+
+
+async def to_ogg(data: bytes, mime: str) -> Optional[bytes]:
+    default_ext = mimetypes.guess_extension(mime)
+    with tempfile.TemporaryDirectory(prefix="mxfb_audio_") as tmpdir:
+        input_file_name = os.path.join(tmpdir, f"input{default_ext}")
+        output_file_name = os.path.join(tmpdir, "output.ogg")
+        with open(input_file_name, "wb") as file:
+            file.write(data)
+        proc = await asyncio.create_subprocess_exec(
+            "ffmpeg",
+            "-i",
+            input_file_name,
+            "-c:a",
+            "libvorbis",
+            output_file_name,
+            stdout=asyncio.subprocess.PIPE,
+            stdin=asyncio.subprocess.PIPE,
+        )
+        _, stderr = await proc.communicate()
+        if proc.returncode == 0:
+            with open(output_file_name, "rb") as file:
+                return file.read()
+        else:
+            err_text = (
+                stderr.decode("utf-8")
+                if stderr is not None
+                else f"unknown ({proc.returncode})"
+            )
+            log.error(f"ffmpeg error: {err_text}")
+    return None