mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-04 20:43:04 +00:00
Add comprehensive backend services and API enhancements
- Complete Spotify integration with downloader and settings - Advanced UX features and audio quality management - Enhanced search capabilities and mobile offline support - Music catalog browser and recap features - Universal downloader and upload functionality - Update tracking system with database models and migrations - Comprehensive service layer architecture - Enhanced lyrics API and streaming capabilities - Extended application builder and startup configuration - New logging infrastructure and services directory
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Contains all the track routes.
|
||||
Contains all the track routes with iOS compatibility enhancements.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -18,6 +18,7 @@ from swingmusic.lib.trackslib import get_silence_paddings
|
||||
|
||||
from swingmusic.store.tracks import TrackStore
|
||||
from swingmusic.utils.files import guess_mime_type
|
||||
from swingmusic.services.ios_audio_compatibility import ios_audio_manager
|
||||
|
||||
bp_tag = Tag(name="File", description="Audio files")
|
||||
api = APIBlueprint("track", __name__, url_prefix="/file", abp_tags=[bp_tag])
|
||||
@@ -54,11 +55,12 @@ class SendTrackFileQuery(BaseModel):
|
||||
@api.get("/<trackhash>/legacy")
|
||||
def send_track_file_legacy(path: TrackHashSchema, query: SendTrackFileQuery):
|
||||
"""
|
||||
Get a playable audio file without Range support
|
||||
Get a playable audio file without Range support (iOS compatible)
|
||||
|
||||
Returns a playable audio file that corresponds to the given filepath. Falls back to track hash if filepath is not found.
|
||||
Automatically handles iOS compatibility by transcoding to supported formats when needed.
|
||||
|
||||
NOTE: Does not support range requests or transcoding.
|
||||
NOTE: Does not support range requests or transcoding beyond iOS compatibility.
|
||||
"""
|
||||
requested_trackhash = path.trackhash.strip()
|
||||
filepath = query.filepath.strip()
|
||||
@@ -106,14 +108,165 @@ def send_track_file_legacy(path: TrackHashSchema, query: SendTrackFileQuery):
|
||||
break
|
||||
|
||||
if track is not None:
|
||||
audio_type = guess_mime_type(track.filepath)
|
||||
return send_from_directory(
|
||||
Path(track.filepath).parent,
|
||||
Path(track.filepath).name,
|
||||
# Detect iOS capabilities and handle compatibility
|
||||
user_agent = request.headers.get('User-Agent', '')
|
||||
ios_capabilities = ios_audio_manager.detect_ios_capabilities(user_agent)
|
||||
|
||||
# Create iOS-compatible audio source
|
||||
audio_source = ios_audio_manager.create_ios_audio_source(
|
||||
track.filepath,
|
||||
ios_capabilities,
|
||||
quality="high"
|
||||
)
|
||||
|
||||
# Use the potentially transcoded file path
|
||||
final_file_path = audio_source['file_path']
|
||||
audio_type = audio_source['mime_type']
|
||||
|
||||
# Add iOS compatibility headers
|
||||
response = send_from_directory(
|
||||
Path(final_file_path).parent,
|
||||
Path(final_file_path).name,
|
||||
mimetype=audio_type,
|
||||
conditional=True,
|
||||
as_attachment=True,
|
||||
)
|
||||
|
||||
# Add iOS-specific headers
|
||||
if ios_capabilities.is_ios:
|
||||
response.headers['Accept-Ranges'] = 'bytes'
|
||||
response.headers['Cache-Control'] = 'public, max-age=3600'
|
||||
|
||||
# Add transcoding info if applicable
|
||||
if audio_source['needs_transcoding']:
|
||||
response.headers['X-iOS-Transcoded'] = 'true'
|
||||
response.headers['X-iOS-Original-Format'] = guess_mime_type(track.filepath)
|
||||
response.headers['X-iOS-Target-Format'] = audio_source['format']
|
||||
|
||||
return response
|
||||
|
||||
return msg, 404
|
||||
|
||||
|
||||
@api.get("/<trackhash>/ios")
|
||||
def send_track_file_ios(path: TrackHashSchema, query: SendTrackFileQuery):
|
||||
"""
|
||||
Get a playable audio file optimized for iOS devices
|
||||
|
||||
Returns a playable audio file optimized for iOS compatibility with automatic transcoding.
|
||||
Supports FLAC to ALAC/AAC conversion and proper MIME types for iOS Safari and other browsers.
|
||||
|
||||
iOS Features:
|
||||
- Automatic FLAC to ALAC/AAC transcoding
|
||||
- Proper MP4 container formatting
|
||||
- iOS-compatible MIME types
|
||||
- Optimized bitrate for mobile streaming
|
||||
- Caching for transcoded files
|
||||
"""
|
||||
requested_trackhash = path.trackhash.strip()
|
||||
filepath = query.filepath.strip()
|
||||
|
||||
msg = {"msg": "File Not Found"}
|
||||
|
||||
# prevent path traversal
|
||||
if "/../" in filepath:
|
||||
return {"msg": "Invalid filepath", "error": "Path traversal detected"}, 400
|
||||
|
||||
requested_filepath = Path(filepath).resolve()
|
||||
|
||||
# check if filepath is a child of any of the root dirs
|
||||
for root_dir in UserConfig().rootDirs:
|
||||
if root_dir == "$home":
|
||||
root_dir = Path.home()
|
||||
else:
|
||||
root_dir = Path(root_dir).resolve()
|
||||
|
||||
if root_dir not in requested_filepath.parents:
|
||||
return {
|
||||
"msg": "Invalid filepath",
|
||||
"error": "File not inside root directories",
|
||||
}, 400
|
||||
|
||||
track = None
|
||||
tracks = TrackStore.get_tracks_by_filepaths([filepath])
|
||||
|
||||
if len(tracks) > 0 and os.path.exists(tracks[0].filepath):
|
||||
for t in tracks:
|
||||
if os.path.exists(t.filepath) and t.trackhash == requested_trackhash:
|
||||
track = t
|
||||
break
|
||||
else:
|
||||
group = TrackStore.trackhashmap.get(requested_trackhash)
|
||||
|
||||
# When finding by trackhash, sort by bitrate
|
||||
# and get the first track that exists
|
||||
if group is not None:
|
||||
tracks = sorted(group.tracks, key=lambda x: x.bitrate, reverse=True)
|
||||
|
||||
for t in tracks:
|
||||
if os.path.exists(t.filepath):
|
||||
track = t
|
||||
break
|
||||
|
||||
if track is not None:
|
||||
# Detect iOS capabilities
|
||||
user_agent = request.headers.get('User-Agent', '')
|
||||
ios_capabilities = ios_audio_manager.detect_ios_capabilities(user_agent)
|
||||
|
||||
# Determine quality based on query parameter or device capabilities
|
||||
quality_map = {
|
||||
'original': 'lossless',
|
||||
'1411': 'lossless',
|
||||
'1024': 'lossless',
|
||||
'512': 'high',
|
||||
'320': 'high',
|
||||
'256': 'high',
|
||||
'128': 'medium',
|
||||
'96': 'low'
|
||||
}
|
||||
quality = quality_map.get(query.quality, 'high')
|
||||
|
||||
# Create iOS-optimized audio source
|
||||
audio_source = ios_audio_manager.create_ios_audio_source(
|
||||
track.filepath,
|
||||
ios_capabilities,
|
||||
quality=quality
|
||||
)
|
||||
|
||||
# Use the potentially transcoded file path
|
||||
final_file_path = audio_source['file_path']
|
||||
audio_type = audio_source['mime_type']
|
||||
|
||||
# Create response with iOS-specific optimizations
|
||||
response = send_from_directory(
|
||||
Path(final_file_path).parent,
|
||||
Path(final_file_path).name,
|
||||
mimetype=audio_type,
|
||||
conditional=True,
|
||||
as_attachment=False, # Stream inline for iOS
|
||||
)
|
||||
|
||||
# iOS-specific headers for optimal playback
|
||||
response.headers['Accept-Ranges'] = 'bytes'
|
||||
response.headers['Cache-Control'] = 'public, max-age=7200' # 2 hours
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
|
||||
# Add iOS compatibility information
|
||||
if ios_capabilities.is_ios:
|
||||
response.headers['X-iOS-Optimized'] = 'true'
|
||||
response.headers['X-iOS-Device'] = 'iPhone' if 'iPhone' in user_agent else 'iPad' if 'iPad' in user_agent else 'iPod'
|
||||
|
||||
# Add transcoding information
|
||||
if audio_source['needs_transcoding']:
|
||||
response.headers['X-iOS-Transcoded'] = 'true'
|
||||
response.headers['X-iOS-Original-Format'] = guess_mime_type(track.filepath)
|
||||
response.headers['X-iOS-Target-Format'] = audio_source['format']
|
||||
response.headers['X-iOS-Quality'] = quality
|
||||
else:
|
||||
response.headers['X-iOS-Transcoded'] = 'false'
|
||||
response.headers['X-iOS-Native-Format'] = 'true'
|
||||
|
||||
return response
|
||||
|
||||
return msg, 404
|
||||
|
||||
|
||||
Reference in New Issue
Block a user