feat(ringtone): implement ringtone management features including upload, retrieval, and deletion; enhance identity management with new API endpoints for creating, switching, and deleting identities
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -131,6 +131,10 @@ class ConfigManager:
|
||||
60,
|
||||
)
|
||||
|
||||
# ringtone config
|
||||
self.custom_ringtone_enabled = self.BoolConfig(self, "custom_ringtone_enabled", False)
|
||||
self.ringtone_filename = self.StringConfig(self, "ringtone_filename", None)
|
||||
|
||||
# map config
|
||||
self.map_offline_enabled = self.BoolConfig(self, "map_offline_enabled", False)
|
||||
self.map_offline_path = self.StringConfig(self, "map_offline_path", None)
|
||||
@@ -183,7 +187,7 @@ class ConfigManager:
|
||||
config_value = self.manager.get(self.key, default_value=None)
|
||||
if config_value is None:
|
||||
return self.default_value
|
||||
return config_value == "true"
|
||||
return str(config_value).lower() == "true"
|
||||
|
||||
def set(self, value: bool):
|
||||
self.manager.set(self.key, "true" if value else "false")
|
||||
|
||||
@@ -8,6 +8,7 @@ from .schema import DatabaseSchema
|
||||
from .telemetry import TelemetryDAO
|
||||
from .telephone import TelephoneDAO
|
||||
from .voicemails import VoicemailDAO
|
||||
from .ringtones import RingtoneDAO
|
||||
|
||||
|
||||
class Database:
|
||||
@@ -21,6 +22,7 @@ class Database:
|
||||
self.telephone = TelephoneDAO(self.provider)
|
||||
self.telemetry = TelemetryDAO(self.provider)
|
||||
self.voicemails = VoicemailDAO(self.provider)
|
||||
self.ringtones = RingtoneDAO(self.provider)
|
||||
|
||||
def initialize(self):
|
||||
self.schema.initialize()
|
||||
|
||||
@@ -18,6 +18,13 @@ class ConfigDAO:
|
||||
self.provider.execute("DELETE FROM config WHERE key = ?", (key,))
|
||||
else:
|
||||
now = datetime.now(UTC)
|
||||
|
||||
# handle booleans specifically to ensure they are stored as "true"/"false"
|
||||
if isinstance(value, bool):
|
||||
value_str = "true" if value else "false"
|
||||
else:
|
||||
value_str = str(value)
|
||||
|
||||
self.provider.execute(
|
||||
"""
|
||||
INSERT INTO config (key, value, created_at, updated_at)
|
||||
@@ -26,7 +33,7 @@ class ConfigDAO:
|
||||
value = EXCLUDED.value,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
""",
|
||||
(key, str(value), now, now),
|
||||
(key, value_str, now, now),
|
||||
)
|
||||
|
||||
def delete(self, key):
|
||||
|
||||
@@ -18,6 +18,10 @@ class DatabaseProvider:
|
||||
msg = "Database path must be provided for the first initialization"
|
||||
raise ValueError(msg)
|
||||
cls._instance = cls(db_path)
|
||||
elif db_path is not None and cls._instance.db_path != db_path:
|
||||
# If a different path is provided, close the old one and create new
|
||||
cls._instance.close()
|
||||
cls._instance = cls(db_path)
|
||||
return cls._instance
|
||||
|
||||
@property
|
||||
|
||||
64
meshchatx/src/backend/database/ringtones.py
Normal file
64
meshchatx/src/backend/database/ringtones.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from datetime import UTC, datetime
|
||||
from .provider import DatabaseProvider
|
||||
|
||||
class RingtoneDAO:
|
||||
def __init__(self, provider: DatabaseProvider):
|
||||
self.provider = provider
|
||||
|
||||
def get_all(self):
|
||||
return self.provider.fetchall("SELECT * FROM ringtones ORDER BY created_at DESC")
|
||||
|
||||
def get_by_id(self, ringtone_id):
|
||||
return self.provider.fetchone("SELECT * FROM ringtones WHERE id = ?", (ringtone_id,))
|
||||
|
||||
def get_primary(self):
|
||||
return self.provider.fetchone("SELECT * FROM ringtones WHERE is_primary = 1")
|
||||
|
||||
def add(self, filename, storage_filename, display_name=None):
|
||||
now = datetime.now(UTC)
|
||||
if display_name is None:
|
||||
display_name = filename
|
||||
|
||||
# check if this is the first ringtone, if so make it primary
|
||||
count = self.provider.fetchone("SELECT COUNT(*) as count FROM ringtones")["count"]
|
||||
is_primary = 1 if count == 0 else 0
|
||||
|
||||
cursor = self.provider.execute(
|
||||
"INSERT INTO ringtones (filename, display_name, storage_filename, is_primary, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
(filename, display_name, storage_filename, is_primary, now, now)
|
||||
)
|
||||
return cursor.lastrowid
|
||||
|
||||
def update(self, ringtone_id, display_name=None, is_primary=None):
|
||||
now = datetime.now(UTC)
|
||||
if is_primary == 1:
|
||||
# reset others
|
||||
self.provider.execute("UPDATE ringtones SET is_primary = 0, updated_at = ?", (now,))
|
||||
|
||||
if display_name is not None and is_primary is not None:
|
||||
self.provider.execute(
|
||||
"UPDATE ringtones SET display_name = ?, is_primary = ?, updated_at = ? WHERE id = ?",
|
||||
(display_name, is_primary, now, ringtone_id)
|
||||
)
|
||||
elif display_name is not None:
|
||||
self.provider.execute(
|
||||
"UPDATE ringtones SET display_name = ?, updated_at = ? WHERE id = ?",
|
||||
(display_name, now, ringtone_id)
|
||||
)
|
||||
elif is_primary is not None:
|
||||
self.provider.execute(
|
||||
"UPDATE ringtones SET is_primary = ?, updated_at = ? WHERE id = ?",
|
||||
(is_primary, now, ringtone_id)
|
||||
)
|
||||
|
||||
def delete(self, ringtone_id):
|
||||
# if deleting primary, make another one primary if exists
|
||||
ringtone = self.get_by_id(ringtone_id)
|
||||
if ringtone and ringtone["is_primary"] == 1:
|
||||
self.provider.execute("DELETE FROM ringtones WHERE id = ?", (ringtone_id,))
|
||||
next_ringtone = self.provider.fetchone("SELECT id FROM ringtones LIMIT 1")
|
||||
if next_ringtone:
|
||||
self.update(next_ringtone["id"], is_primary=1)
|
||||
else:
|
||||
self.provider.execute("DELETE FROM ringtones WHERE id = ?", (ringtone_id,))
|
||||
|
||||
@@ -2,7 +2,7 @@ from .provider import DatabaseProvider
|
||||
|
||||
|
||||
class DatabaseSchema:
|
||||
LATEST_VERSION = 16
|
||||
LATEST_VERSION = 17
|
||||
|
||||
def __init__(self, provider: DatabaseProvider):
|
||||
self.provider = provider
|
||||
@@ -227,6 +227,17 @@ class DatabaseSchema:
|
||||
UNIQUE(destination_hash, timestamp)
|
||||
)
|
||||
""",
|
||||
"ringtones": """
|
||||
CREATE TABLE IF NOT EXISTS ringtones (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT,
|
||||
display_name TEXT,
|
||||
storage_filename TEXT,
|
||||
is_primary INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""",
|
||||
}
|
||||
|
||||
for table_name, create_sql in tables.items():
|
||||
@@ -501,6 +512,19 @@ class DatabaseSchema:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if current_version < 17:
|
||||
self.provider.execute("""
|
||||
CREATE TABLE IF NOT EXISTS ringtones (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT,
|
||||
display_name TEXT,
|
||||
storage_filename TEXT,
|
||||
is_primary INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# Update version in config
|
||||
self.provider.execute(
|
||||
"""
|
||||
|
||||
@@ -45,3 +45,6 @@ class TelephoneDAO:
|
||||
"SELECT * FROM call_history ORDER BY timestamp DESC LIMIT ?",
|
||||
(limit,),
|
||||
)
|
||||
|
||||
def clear_call_history(self):
|
||||
self.provider.execute("DELETE FROM call_history")
|
||||
|
||||
64
meshchatx/src/backend/ringtone_manager.py
Normal file
64
meshchatx/src/backend/ringtone_manager.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import RNS
|
||||
|
||||
class RingtoneManager:
|
||||
def __init__(self, config, storage_dir):
|
||||
self.config = config
|
||||
self.storage_dir = os.path.join(storage_dir, "ringtones")
|
||||
|
||||
# Ensure directory exists
|
||||
os.makedirs(self.storage_dir, exist_ok=True)
|
||||
|
||||
# Paths to executables
|
||||
self.ffmpeg_path = self._find_ffmpeg()
|
||||
self.has_ffmpeg = self.ffmpeg_path is not None
|
||||
|
||||
if self.has_ffmpeg:
|
||||
RNS.log(f"Ringtone: Found ffmpeg at {self.ffmpeg_path}", RNS.LOG_DEBUG)
|
||||
else:
|
||||
RNS.log("Ringtone: ffmpeg not found", RNS.LOG_ERROR)
|
||||
|
||||
def _find_ffmpeg(self):
|
||||
path = shutil.which("ffmpeg")
|
||||
if path:
|
||||
return path
|
||||
return None
|
||||
|
||||
def convert_to_ringtone(self, input_path, ringtone_id=None):
|
||||
if not self.has_ffmpeg:
|
||||
msg = "ffmpeg is required for audio conversion"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
import secrets
|
||||
filename = f"ringtone_{secrets.token_hex(8)}.opus"
|
||||
opus_path = os.path.join(self.storage_dir, filename)
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
self.ffmpeg_path,
|
||||
"-i",
|
||||
input_path,
|
||||
"-c:a",
|
||||
"libopus",
|
||||
"-b:a",
|
||||
"32k",
|
||||
"-vbr",
|
||||
"on",
|
||||
opus_path,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
return filename
|
||||
|
||||
def remove_ringtone(self, filename):
|
||||
opus_path = os.path.join(self.storage_dir, filename)
|
||||
if os.path.exists(opus_path):
|
||||
os.remove(opus_path)
|
||||
return True
|
||||
|
||||
def get_ringtone_path(self, filename):
|
||||
return os.path.join(self.storage_dir, filename)
|
||||
|
||||
Reference in New Issue
Block a user