Procházet zdrojové kódy

voice messages: convert incoming voice messages to ogg

Sumner Evans před 3 roky
rodič
revize
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.errors import MatrixError, MForbidden, MNotFound, SessionNotFound
 from mautrix.util.simple_lock import SimpleLock
 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 .db import Portal as DBPortal, Message as DBMessage, Reaction as DBReaction
 from .config import Config
 from .config import Config
@@ -465,10 +467,18 @@ class Portal(DBPortal, BasePortal):
 
 
     async def _reupload_instagram_voice(self, source: 'u.User', media: VoiceMediaItem,
     async def _reupload_instagram_voice(self, source: 'u.User', media: VoiceMediaItem,
                                         intent: IntentAPI) -> MediaMessageEventContent:
                                         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
         url = media.media.audio.audio_src
         info = AudioInfo(duration=media.media.audio.duration)
         info = AudioInfo(duration=media.media.audio.duration)
         waveform = [int(p * 1000) for p in media.media.audio.waveform_data]
         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"] = {
         content["org.matrix.msc1767.file"] = {
             "url": content.url,
             "url": content.url,
             "name": content.body,
             "name": content.body,
@@ -482,9 +492,10 @@ class Portal(DBPortal, BasePortal):
         content["org.matrix.msc3245.voice"] = {}
         content["org.matrix.msc3245.voice"] = {}
         return content
         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:
         async with await source.client.raw_http_get(url) as resp:
             try:
             try:
                 length = int(resp.headers["Content-Length"])
                 length = int(resp.headers["Content-Length"])
@@ -499,12 +510,18 @@ class Portal(DBPortal, BasePortal):
                 raise ValueError("Attachment not available: too large")
                 raise ValueError("Attachment not available: too large")
             data = await resp.read()
             data = await resp.read()
             info.mimetype = resp.headers["Content-Type"] or magic.from_buffer(data, mime=True)
             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)
         info.size = len(data)
         extension = {
         extension = {
             "image/webp": ".webp",
             "image/webp": ".webp",
             "image/jpeg": ".jpg",
             "image/jpeg": ".jpg",
             "video/mp4": ".mp4",
             "video/mp4": ".mp4",
             "audio/mp4": ".m4a",
             "audio/mp4": ".m4a",
+            "audio/ogg": ".ogg",
         }.get(info.mimetype)
         }.get(info.mimetype)
         extension = extension or mimetypes.guess_extension(info.mimetype) or ""
         extension = extension or mimetypes.guess_extension(info.mimetype) or ""
         file_name = f"{msgtype.value[2:]}{extension}"
         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