mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
38f1981283
- Move all backend files from swingmusic/ to root level - Backend files now display directly on GitHub repository page - Keep client applications as submodules (swingmusic-android, swingmusic-desktop, swingmusic-webclient) - Update README to reflect new structure (no cd swingmusic needed) - Cleaner, more professional GitHub repository layout Files moved to root: - src/ (main source code) - pyproject.toml, requirements.txt, run.py - swingmusic.spec, uv.lock, version.txt - services/ Result: GitHub shows backend files directly while maintaining organized structure
229 lines
8.6 KiB
Python
229 lines
8.6 KiB
Python
"""
|
|
iOS Audio Compatibility Service for SwingMusic
|
|
Handles iOS-specific audio playback issues and format compatibility
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
from typing import Optional, Dict, Any, Tuple
|
|
from pathlib import Path
|
|
from dataclasses import dataclass
|
|
|
|
from swingmusic import logger
|
|
from swingmusic.utils.files import guess_mime_type
|
|
|
|
|
|
@dataclass
|
|
class IOSAudioCapabilities:
|
|
"""iOS device audio capabilities"""
|
|
is_safari: bool
|
|
is_ios: bool
|
|
supports_flac: bool
|
|
supports_webm: bool
|
|
supports_alac: bool
|
|
supports_aac: bool
|
|
user_agent: str
|
|
optimal_format: str
|
|
optimal_codec: str
|
|
|
|
|
|
class IOSAudioManager:
|
|
"""Manages iOS audio compatibility and transcoding"""
|
|
|
|
def __init__(self):
|
|
self.temp_dir = tempfile.gettempdir()
|
|
self.transcode_cache = {}
|
|
|
|
def detect_ios_capabilities(self, user_agent: str = "") -> IOSAudioCapabilities:
|
|
"""Detect iOS device capabilities from user agent"""
|
|
is_safari = 'Safari' in user_agent and 'Chrome' not in user_agent
|
|
is_ios = bool(re.search(r'iPad|iPhone|iPod', user_agent))
|
|
|
|
# iOS format support matrix
|
|
supports_flac = False # iOS doesn't support FLAC natively
|
|
supports_webm = False # Limited WebM support on iOS
|
|
supports_alac = True # Apple Lossless supported on iOS
|
|
supports_aac = True # AAC widely supported
|
|
|
|
# Determine optimal format for iOS
|
|
if is_ios:
|
|
if supports_alac:
|
|
optimal_format = 'mp4' # ALAC in MP4 container
|
|
optimal_codec = 'alac'
|
|
else:
|
|
optimal_format = 'mp4' # AAC in MP4 container
|
|
optimal_codec = 'aac'
|
|
else:
|
|
optimal_format = 'flac' # Use original format for non-iOS
|
|
optimal_codec = 'flac'
|
|
|
|
return IOSAudioCapabilities(
|
|
is_safari=is_safari,
|
|
is_ios=is_ios,
|
|
supports_flac=supports_flac,
|
|
supports_webm=supports_webm,
|
|
supports_alac=supports_alac,
|
|
supports_aac=supports_aac,
|
|
user_agent=user_agent,
|
|
optimal_format=optimal_format,
|
|
optimal_codec=optimal_codec
|
|
)
|
|
|
|
def needs_transcoding(self, file_path: str, capabilities: IOSAudioCapabilities) -> bool:
|
|
"""Check if file needs transcoding for iOS compatibility"""
|
|
if not capabilities.is_ios:
|
|
return False
|
|
|
|
original_mime = guess_mime_type(file_path)
|
|
|
|
# iOS doesn't support FLAC, need transcoding
|
|
if original_mime == 'audio/flac' and not capabilities.supports_flac:
|
|
return True
|
|
|
|
# iOS has limited WebM support
|
|
if original_mime == 'audio/webm' and not capabilities.supports_webm:
|
|
return True
|
|
|
|
return False
|
|
|
|
def get_optimal_audio_format(self, file_path: str, capabilities: IOSAudioCapabilities) -> Tuple[str, str]:
|
|
"""Get optimal audio format and codec for the device"""
|
|
if not capabilities.is_ios:
|
|
# Return original format for non-iOS devices
|
|
original_mime = guess_mime_type(file_path)
|
|
if original_mime == 'audio/flac':
|
|
return 'flac', 'flac'
|
|
elif original_mime == 'audio/mpeg':
|
|
return 'mp3', 'mp3'
|
|
else:
|
|
return 'mp4', 'aac'
|
|
|
|
# Return iOS-optimized format
|
|
return capabilities.optimal_format, capabilities.optimal_codec
|
|
|
|
def transcode_for_ios(self, file_path: str, capabilities: IOSAudioCapabilities,
|
|
quality: str = "high") -> Optional[str]:
|
|
"""Transcode audio file for iOS compatibility"""
|
|
try:
|
|
# Check if already transcoded
|
|
cache_key = f"{file_path}_{capabilities.optimal_format}_{quality}"
|
|
if cache_key in self.transcode_cache:
|
|
cached_file = self.transcode_cache[cache_key]
|
|
if os.path.exists(cached_file):
|
|
return cached_file
|
|
|
|
# Create output file path
|
|
input_path = Path(file_path)
|
|
output_filename = f"{input_path.stem}_ios_{capabilities.optimal_format}.{capabilities.optimal_format}"
|
|
output_path = os.path.join(self.temp_dir, output_filename)
|
|
|
|
# Prepare FFmpeg command based on target format
|
|
if capabilities.optimal_codec == 'alac':
|
|
# Apple Lossless Audio Codec
|
|
cmd = [
|
|
'ffmpeg', '-i', file_path,
|
|
'-c:a', 'alac',
|
|
'-ar', '44100', # Sample rate
|
|
'-ac', '2', # Stereo
|
|
'-y', output_path
|
|
]
|
|
elif capabilities.optimal_codec == 'aac':
|
|
# AAC codec with quality settings
|
|
bitrate_map = {
|
|
'low': '96k',
|
|
'medium': '128k',
|
|
'high': '256k',
|
|
'lossless': '320k'
|
|
}
|
|
bitrate = bitrate_map.get(quality, '256k')
|
|
|
|
cmd = [
|
|
'ffmpeg', '-i', file_path,
|
|
'-c:a', 'aac',
|
|
'-b:a', bitrate,
|
|
'-ar', '44100',
|
|
'-ac', '2',
|
|
'-y', output_path
|
|
]
|
|
else:
|
|
# Default to AAC
|
|
cmd = [
|
|
'ffmpeg', '-i', file_path,
|
|
'-c:a', 'aac',
|
|
'-b:a', '256k',
|
|
'-ar', '44100',
|
|
'-ac', '2',
|
|
'-y', output_path
|
|
]
|
|
|
|
# Execute transcoding
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode == 0 and os.path.exists(output_path):
|
|
# Cache the transcoded file
|
|
self.transcode_cache[cache_key] = output_path
|
|
logger.info(f"Successfully transcoded {file_path} for iOS: {output_path}")
|
|
return output_path
|
|
else:
|
|
logger.error(f"FFmpeg transcoding failed: {result.stderr}")
|
|
return None
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error transcoding for iOS: {e}")
|
|
return None
|
|
|
|
def get_ios_compatible_mime_type(self, file_path: str, capabilities: IOSAudioCapabilities) -> str:
|
|
"""Get iOS-compatible MIME type"""
|
|
if not capabilities.is_ios:
|
|
return guess_mime_type(file_path)
|
|
|
|
if capabilities.optimal_format == 'mp4':
|
|
if capabilities.optimal_codec == 'alac':
|
|
return 'audio/mp4' # ALAC in MP4 container
|
|
else:
|
|
return 'audio/mp4' # AAC in MP4 container
|
|
elif capabilities.optimal_format == 'mp3':
|
|
return 'audio/mpeg'
|
|
else:
|
|
return 'audio/mp4' # Default to MP4 container for iOS
|
|
|
|
def create_ios_audio_source(self, file_path: str, capabilities: IOSAudioCapabilities,
|
|
quality: str = "high") -> Dict[str, Any]:
|
|
"""Create iOS-compatible audio source configuration"""
|
|
source_config = {
|
|
'file_path': file_path,
|
|
'needs_transcoding': self.needs_transcoding(file_path, capabilities),
|
|
'mime_type': self.get_ios_compatible_mime_type(file_path, capabilities),
|
|
'format': capabilities.optimal_format,
|
|
'codec': capabilities.optimal_codec
|
|
}
|
|
|
|
if source_config['needs_transcoding']:
|
|
transcoded_path = self.transcode_for_ios(file_path, capabilities, quality)
|
|
if transcoded_path:
|
|
source_config['transcoded_path'] = transcoded_path
|
|
source_config['file_path'] = transcoded_path
|
|
else:
|
|
# Fallback to original file if transcoding fails
|
|
logger.warning(f"Transcoding failed, using original file: {file_path}")
|
|
source_config['needs_transcoding'] = False
|
|
source_config['mime_type'] = guess_mime_type(file_path)
|
|
|
|
return source_config
|
|
|
|
def cleanup_transcoded_files(self):
|
|
"""Clean up temporary transcoded files"""
|
|
try:
|
|
for cached_file in self.transcode_cache.values():
|
|
if os.path.exists(cached_file):
|
|
os.remove(cached_file)
|
|
self.transcode_cache.clear()
|
|
except Exception as e:
|
|
logger.error(f"Error cleaning up transcoded files: {e}")
|
|
|
|
|
|
# Global iOS audio manager instance
|
|
ios_audio_manager = IOSAudioManager()
|