Compare commits

...
Author SHA1 Message Date
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
4 changed files with 154 additions and 9 deletions
+13 -4
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
- **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
- **Instant App Restore** - App loads instantly with cached library data while syncing in background
- **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
### Theming
@@ -94,11 +104,10 @@ Ensemble supports multiple authentication methods:
## 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
- Android device (Android 5.0+)
> **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.
- Audiobookshelf provider configured in Music Assistant (for audiobook features)
## License
+97 -1
View File
@@ -32,6 +32,8 @@ class _LoginScreenState extends State<LoginScreen> {
String? _error;
bool _showDebug = false;
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
void initState() {
@@ -96,10 +98,27 @@ class _LoginScreenState extends State<LoginScreen> {
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://
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() {
// Normalize the base URL
var url = _normalizeServerUrl(_serverUrlController.text.trim());
@@ -149,6 +168,8 @@ class _LoginScreenState extends State<LoginScreen> {
_error = null;
_detectedAuthStrategy = null;
_detectedAuthType = null;
_resolvedServerUrl = null;
_usedHttpFallback = false;
_debugLogs.clear();
});
@@ -162,6 +183,13 @@ class _LoginScreenState extends State<LoginScreen> {
// Auto-detect authentication requirements
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'}');
if (strategy == null) {
@@ -175,6 +203,7 @@ class _LoginScreenState extends State<LoginScreen> {
setState(() {
_detectedAuthStrategy = strategy;
_detectedAuthType = _getAuthTypeName(strategy.name);
_resolvedServerUrl = resolvedUrl;
_isDetectingAuth = false;
});
@@ -252,7 +281,9 @@ class _LoginScreenState extends State<LoginScreen> {
});
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();
// 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)
if (needsAuth) ...[
// Auth Server URL (only for Authelia)
+43 -3
View File
@@ -31,11 +31,42 @@ class AuthManager {
/// Auto-detect authentication requirements for a server
/// 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 {
_logger.log('🔍 Auto-detecting auth strategy for $serverUrl');
_lastSuccessfulUrl = null;
// 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://')) {
// Use http for local IPs, https for domains
if (baseUrl.startsWith('192.') ||
@@ -44,13 +75,22 @@ class AuthManager {
baseUrl == 'localhost' ||
baseUrl.startsWith('127.')) {
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 {
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
_logger.log('Checking for Music Assistant native auth...');
_logger.log('Checking for Music Assistant native auth at $baseUrl...');
final maAuthResult = await _checkMusicAssistantAuth(baseUrl);
if (maAuthResult != null) {
if (maAuthResult == 'required') {
@@ -124,7 +164,7 @@ class AuthManager {
}
} catch (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');
+1 -1
View File
@@ -1,7 +1,7 @@
name: ensemble
description: A minimalistic Music Assistant client built with Flutter
publish_to: 'none'
version: 2.1.0-beta+16
version: 2.1.1-beta+17
environment:
sdk: '>=3.0.0 <4.0.0'