Compare commits

...
Author SHA1 Message Date
Your Name f03ae2f75b Bump version to 2.1.2-beta 2025-12-19 06:10:19 +00:00
Your NameandClaude Opus 4.5 23fe4a20c7 Fix auth detection for Tailscale/reverse proxy URLs
When checking for MA auth on HTTP URLs without a port, the code was
defaulting to port 8095 (MA's direct port). This caused auth detection
to fail for Tailscale URLs behind a reverse proxy on port 80.

Now uses default HTTP/HTTPS ports (80/443) when no port is specified.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 06:10:00 +00:00
Your Name 90ad14de28 Add cache-busting to screenshot URLs 2025-12-19 05:57:33 +00:00
Your NameandClaude Opus 4.5 b3562fee0c Update screenshots and make them more compact in README
- Updated screenshots 3, 4, 5 with new captures
- Added new screenshots 7 (Audiobooks) and 8 (Audiobook Player)
- Reduced screenshot width from 250px to 150px for compact display
- Arranged all 8 screenshots in a single row that wraps naturally

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 05:52:38 +00:00
Your NameandClaude Opus 4.5 baa6299130 Bump version to 2.1.1-beta
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 05:07:22 +00:00
Your NameandClaude Opus 4.5 e6a02c191e Improve Tailscale and HTTP connection handling
- Add Tailscale URL detection (*.ts.net) to default to HTTP instead of HTTPS,
  since the VPN tunnel already provides encryption
- Implement HTTPS to HTTP automatic fallback when HTTPS connection fails
- Add HTTP connection info banner on login screen:
  - Shows "Tailscale VPN Connection" with VPN lock icon for *.ts.net URLs
  - Shows "Unencrypted Connection" warning for other HTTP connections
- Track resolved server URL from fallback and use it for actual connection

Fixes user-reported issue where connecting via Tailscale URL required
manually adding "http://" prefix and specifying port 80.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-19 04:48:59 +00:00
Your NameandClaude Opus 4.5 7d7e5266c7 Update README with audiobooks and new features
- Add Audiobooks section with full feature list
- Add Instant App Restore and Offline Browsing to Smart Features
- Update requirements for MA 2.7.0 beta 20+
- Add Audiobookshelf requirement for audiobook features
- Remove outdated Sendspin compatibility note

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 22:14:42 +00:00
9 changed files with 165 additions and 19 deletions
+21 -13
View File
@@ -44,9 +44,19 @@ This application was built with AI-assisted development using **Claude Code** an
- **Playlist Support** - Browse and play your playlists - **Playlist Support** - Browse and play your playlists
- **Search** - Find music across your entire library - **Search** - Find music across your entire library
### Audiobooks
- **Audiobooks Tab** - Browse your audiobook library with grid/list view options
- **Series Support** - View audiobooks organized by series with collage cover art
- **Author Browsing** - Browse audiobooks by author
- **Chapter Navigation** - Jump between chapters with timestamp display
- **Progress Tracking** - Track your listening progress across sessions
- **Continue Listening** - Pick up where you left off
- **Mark as Finished/Unplayed** - Manage your reading progress
### Smart Features ### Smart Features
- **Instant App Restore** - App loads instantly with cached library data while syncing in background
- **Auto-Reconnect** - Automatically reconnects when connection is lost - **Auto-Reconnect** - Automatically reconnects when connection is lost
- **Caching** - Fast navigation with cached content - **Offline Browsing** - Browse your cached library even when disconnected
- **Hero Animations** - Smooth transitions between screens - **Hero Animations** - Smooth transitions between screens
### Theming ### Theming
@@ -57,15 +67,14 @@ This application was built with AI-assisted development using **Claude Code** an
## Screenshots ## Screenshots
<div align="center"> <div align="center">
<img src="assets/screenshots/1.png" alt="Connection Screen" width="250"> <img src="assets/screenshots/1.png?v=2" alt="Connection Screen" width="150">
<img src="assets/screenshots/2.png" alt="Home Screen" width="250"> <img src="assets/screenshots/2.png?v=2" alt="Home Screen" width="150">
<img src="assets/screenshots/3.png" alt="Album Details" width="250"> <img src="assets/screenshots/3.png?v=2" alt="Album Details" width="150">
</div> <img src="assets/screenshots/4.png?v=2" alt="Now Playing" width="150">
<img src="assets/screenshots/5.png?v=2" alt="Queue" width="150">
<div align="center"> <img src="assets/screenshots/6.png?v=2" alt="Settings" width="150">
<img src="assets/screenshots/4.png" alt="Now Playing" width="250"> <img src="assets/screenshots/7.png?v=2" alt="Audiobooks" width="150">
<img src="assets/screenshots/5.png" alt="Device Selection" width="250"> <img src="assets/screenshots/8.png?v=2" alt="Audiobook Player" width="150">
<img src="assets/screenshots/6.png" alt="Settings" width="250">
</div> </div>
## Download ## Download
@@ -94,11 +103,10 @@ Ensemble supports multiple authentication methods:
## Requirements ## Requirements
- Music Assistant server (v2.7.0 or later recommended) - Music Assistant server (v2.7.0 beta 20 or later)
- Network connectivity to your Music Assistant server - Network connectivity to your Music Assistant server
- Android device (Android 5.0+) - Android device (Android 5.0+)
- Audiobookshelf provider configured in Music Assistant (for audiobook features)
> **Note:** Local device playback (streaming to your phone) is not currently supported on Music Assistant beta 20+. This is due to MA replacing the built-in player with Sendspin. Support will be added once Sendspin is more stable. Remote control of other players works fine.
## License ## License
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

+97 -1
View File
@@ -32,6 +32,8 @@ class _LoginScreenState extends State<LoginScreen> {
String? _error; String? _error;
bool _showDebug = false; bool _showDebug = false;
List<String> _debugLogs = []; List<String> _debugLogs = [];
String? _resolvedServerUrl; // The actual URL after fallback (e.g., HTTP if HTTPS failed)
bool _usedHttpFallback = false; // True if HTTPS failed and we fell back to HTTP
@override @override
void initState() { void initState() {
@@ -96,10 +98,27 @@ class _LoginScreenState extends State<LoginScreen> {
return 'http://$url'; return 'http://$url';
} }
// Tailscale URLs (*.ts.net) - default to http:// since VPN tunnel is already encrypted
if (url.endsWith('.ts.net') || url.contains('.ts.net:')) {
return 'http://$url';
}
// For domain names, default to https:// // For domain names, default to https://
return 'https://$url'; return 'https://$url';
} }
/// Check if the URL is using HTTP (for security warning display)
bool _isHttpConnection(String url) {
final normalized = _normalizeServerUrl(url);
return normalized.startsWith('http://') && !normalized.startsWith('https://');
}
/// Check if the URL is a Tailscale URL (safe for HTTP)
bool _isTailscaleUrl(String url) {
final normalized = url.trim().toLowerCase();
return normalized.contains('.ts.net');
}
String _buildServerUrl() { String _buildServerUrl() {
// Normalize the base URL // Normalize the base URL
var url = _normalizeServerUrl(_serverUrlController.text.trim()); var url = _normalizeServerUrl(_serverUrlController.text.trim());
@@ -149,6 +168,8 @@ class _LoginScreenState extends State<LoginScreen> {
_error = null; _error = null;
_detectedAuthStrategy = null; _detectedAuthStrategy = null;
_detectedAuthType = null; _detectedAuthType = null;
_resolvedServerUrl = null;
_usedHttpFallback = false;
_debugLogs.clear(); _debugLogs.clear();
}); });
@@ -162,6 +183,13 @@ class _LoginScreenState extends State<LoginScreen> {
// Auto-detect authentication requirements // Auto-detect authentication requirements
final strategy = await provider.authManager.detectAuthStrategy(serverUrl); final strategy = await provider.authManager.detectAuthStrategy(serverUrl);
// Check if auth manager used a different URL (e.g., HTTP fallback)
final resolvedUrl = provider.authManager.lastSuccessfulUrl;
if (resolvedUrl != null && resolvedUrl != serverUrl) {
_addDebugLog('URL resolved via fallback: $resolvedUrl');
_usedHttpFallback = serverUrl.startsWith('https://') && resolvedUrl.startsWith('http://');
}
_addDebugLog('Detection complete. Strategy: ${strategy?.name ?? 'null'}'); _addDebugLog('Detection complete. Strategy: ${strategy?.name ?? 'null'}');
if (strategy == null) { if (strategy == null) {
@@ -175,6 +203,7 @@ class _LoginScreenState extends State<LoginScreen> {
setState(() { setState(() {
_detectedAuthStrategy = strategy; _detectedAuthStrategy = strategy;
_detectedAuthType = _getAuthTypeName(strategy.name); _detectedAuthType = _getAuthTypeName(strategy.name);
_resolvedServerUrl = resolvedUrl;
_isDetectingAuth = false; _isDetectingAuth = false;
}); });
@@ -252,7 +281,9 @@ class _LoginScreenState extends State<LoginScreen> {
}); });
try { try {
final serverUrl = _buildServerUrl(); // Use the resolved URL if available (e.g., from HTTP fallback), otherwise build it
final serverUrl = _resolvedServerUrl ?? _buildServerUrl();
_addDebugLog('Using server URL for connection: $serverUrl');
final port = _portController.text.trim(); final port = _portController.text.trim();
// Validate port (if provided) // Validate port (if provided)
@@ -635,6 +666,71 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
), ),
// HTTP connection info banner
if (_resolvedServerUrl != null && _isHttpConnection(_resolvedServerUrl!))
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
color: _isTailscaleUrl(_serverUrlController.text)
? colorScheme.secondaryContainer.withOpacity(0.3)
: colorScheme.errorContainer.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
border: _isTailscaleUrl(_serverUrlController.text)
? null
: Border.all(color: colorScheme.error.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
_isTailscaleUrl(_serverUrlController.text)
? Icons.vpn_lock_rounded
: Icons.warning_amber_rounded,
color: _isTailscaleUrl(_serverUrlController.text)
? colorScheme.secondary
: colorScheme.error,
size: 24,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_isTailscaleUrl(_serverUrlController.text)
? 'Tailscale VPN Connection'
: 'Unencrypted Connection',
style: TextStyle(
color: _isTailscaleUrl(_serverUrlController.text)
? colorScheme.onSecondaryContainer
: colorScheme.onErrorContainer,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_isTailscaleUrl(_serverUrlController.text)
? 'Using HTTP over Tailscale (encrypted tunnel)'
: _usedHttpFallback
? 'HTTPS failed, using HTTP fallback'
: 'HTTP connection - data is not encrypted',
style: TextStyle(
color: (_isTailscaleUrl(_serverUrlController.text)
? colorScheme.onSecondaryContainer
: colorScheme.onErrorContainer).withOpacity(0.7),
fontSize: 12,
),
),
),
],
),
),
],
),
),
// Authentication fields (shown only if auth required) // Authentication fields (shown only if auth required)
if (needsAuth) ...[ if (needsAuth) ...[
// Auth Server URL (only for Authelia) // Auth Server URL (only for Authelia)
+46 -4
View File
@@ -31,11 +31,42 @@ class AuthManager {
/// Auto-detect authentication requirements for a server /// Auto-detect authentication requirements for a server
/// Returns null if server is unreachable or auth method unknown /// Returns null if server is unreachable or auth method unknown
/// Also returns the final URL used (in case of HTTP fallback) via the callback
String? _lastSuccessfulUrl;
String? get lastSuccessfulUrl => _lastSuccessfulUrl;
Future<AuthStrategy?> detectAuthStrategy(String serverUrl) async { Future<AuthStrategy?> detectAuthStrategy(String serverUrl) async {
_logger.log('🔍 Auto-detecting auth strategy for $serverUrl'); _logger.log('🔍 Auto-detecting auth strategy for $serverUrl');
_lastSuccessfulUrl = null;
// Normalize URL // Normalize URL
var baseUrl = serverUrl; var baseUrl = _normalizeUrl(serverUrl);
_logger.log('Normalized URL: $baseUrl');
// Try detection with the initial URL
var result = await _tryDetectAuth(baseUrl);
// If HTTPS failed and we haven't tried HTTP yet, try HTTP fallback
if (result == null && baseUrl.startsWith('https://')) {
final httpUrl = baseUrl.replaceFirst('https://', 'http://');
_logger.log('🔄 HTTPS failed, trying HTTP fallback: $httpUrl');
result = await _tryDetectAuth(httpUrl);
if (result != null) {
baseUrl = httpUrl;
}
}
if (result != null) {
_lastSuccessfulUrl = baseUrl;
_logger.log('✅ Auth detection successful with URL: $baseUrl');
}
return result;
}
/// Normalize a server URL with appropriate protocol
String _normalizeUrl(String url) {
var baseUrl = url;
if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) { if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
// Use http for local IPs, https for domains // Use http for local IPs, https for domains
if (baseUrl.startsWith('192.') || if (baseUrl.startsWith('192.') ||
@@ -44,13 +75,22 @@ class AuthManager {
baseUrl == 'localhost' || baseUrl == 'localhost' ||
baseUrl.startsWith('127.')) { baseUrl.startsWith('127.')) {
baseUrl = 'http://$baseUrl'; baseUrl = 'http://$baseUrl';
} else if (baseUrl.endsWith('.ts.net') || baseUrl.contains('.ts.net:')) {
// Tailscale URLs - default to http:// since VPN tunnel is already encrypted
baseUrl = 'http://$baseUrl';
_logger.log('🔗 Detected Tailscale URL, using HTTP');
} else { } else {
baseUrl = 'https://$baseUrl'; baseUrl = 'https://$baseUrl';
} }
} }
return baseUrl;
}
/// Try to detect auth strategy for a given URL
/// Returns the strategy if successful, null if connection failed
Future<AuthStrategy?> _tryDetectAuth(String baseUrl) async {
// First, check if this is a Music Assistant server with native auth // First, check if this is a Music Assistant server with native auth
_logger.log('Checking for Music Assistant native auth...'); _logger.log('Checking for Music Assistant native auth at $baseUrl...');
final maAuthResult = await _checkMusicAssistantAuth(baseUrl); final maAuthResult = await _checkMusicAssistantAuth(baseUrl);
if (maAuthResult != null) { if (maAuthResult != null) {
if (maAuthResult == 'required') { if (maAuthResult == 'required') {
@@ -124,7 +164,7 @@ class AuthManager {
} }
} catch (e) { } catch (e) {
_logger.log('✗ Auth detection error: $e'); _logger.log('✗ Auth detection error: $e');
return null; // Return null instead of rethrow to show generic error return null; // Return null to trigger fallback
} }
_logger.log('⚠️ Could not determine auth method'); _logger.log('⚠️ Could not determine auth method');
@@ -136,10 +176,12 @@ class AuthManager {
Future<String?> _checkMusicAssistantAuth(String baseUrl) async { Future<String?> _checkMusicAssistantAuth(String baseUrl) async {
try { try {
final uri = Uri.parse(baseUrl); final uri = Uri.parse(baseUrl);
// Use the port from URL if specified, otherwise use default ports (80/443)
// Don't default to 8095 here - that's only for direct connections without reverse proxy
final apiUrl = Uri( final apiUrl = Uri(
scheme: uri.scheme, scheme: uri.scheme,
host: uri.host, host: uri.host,
port: uri.hasPort ? uri.port : (uri.scheme == 'https' ? null : 8095), port: uri.hasPort ? uri.port : null,
path: '/api', path: '/api',
); );
+1 -1
View File
@@ -1,7 +1,7 @@
name: ensemble name: ensemble
description: A minimalistic Music Assistant client built with Flutter description: A minimalistic Music Assistant client built with Flutter
publish_to: 'none' publish_to: 'none'
version: 2.1.0-beta+16 version: 2.1.2-beta+18
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'