fix - jellyfin servers can not select a user when determing which library to scan.
This commit is contained in:
parent
51515bc8a1
commit
0d66f884e6
4 changed files with 279 additions and 24 deletions
|
|
@ -205,37 +205,75 @@ class JellyfinClient:
|
||||||
logger.error("No users found on Jellyfin server")
|
logger.error("No users found on Jellyfin server")
|
||||||
return
|
return
|
||||||
|
|
||||||
# LOGIC CHANGE: Iterate through users instead of blindly picking the first one
|
# Check for a saved user preference
|
||||||
|
from database.music_database import MusicDatabase
|
||||||
|
db = MusicDatabase()
|
||||||
|
preferred_user = db.get_preference('jellyfin_user')
|
||||||
|
|
||||||
valid_user_found = False
|
valid_user_found = False
|
||||||
|
|
||||||
for user in users_response:
|
# If a preferred user is saved, try that user first
|
||||||
candidate_id = user['Id']
|
if preferred_user:
|
||||||
candidate_name = user.get('Name', 'Unknown')
|
for user in users_response:
|
||||||
|
if user.get('Name') == preferred_user:
|
||||||
|
candidate_id = user['Id']
|
||||||
|
try:
|
||||||
|
views_response = self._make_request(f'/Users/{candidate_id}/Views')
|
||||||
|
if views_response:
|
||||||
|
for view in views_response.get('Items', []):
|
||||||
|
collection_type = (view.get('CollectionType') or '').lower()
|
||||||
|
if collection_type == 'music':
|
||||||
|
self.user_id = candidate_id
|
||||||
|
self.music_library_id = view['Id']
|
||||||
|
logger.info(f"Using preferred user: {preferred_user} (Music Library: {view.get('Name')})")
|
||||||
|
valid_user_found = True
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Preferred user {preferred_user} failed: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Check for a saved library preference for the selected user
|
||||||
|
preferred_library = db.get_preference('jellyfin_music_library')
|
||||||
|
|
||||||
|
# Fall back to auto-detect if preference is missing or invalid
|
||||||
|
if not valid_user_found:
|
||||||
|
for user in users_response:
|
||||||
|
candidate_id = user['Id']
|
||||||
|
candidate_name = user.get('Name', 'Unknown')
|
||||||
|
|
||||||
|
try:
|
||||||
|
views_response = self._make_request(f'/Users/{candidate_id}/Views')
|
||||||
|
|
||||||
|
if views_response:
|
||||||
|
for view in views_response.get('Items', []):
|
||||||
|
collection_type = (view.get('CollectionType') or '').lower()
|
||||||
|
|
||||||
|
if collection_type == 'music':
|
||||||
|
self.user_id = candidate_id
|
||||||
|
self.music_library_id = view['Id']
|
||||||
|
logger.info(f"Using user: {candidate_name} (Music Library: {view.get('Name')})")
|
||||||
|
valid_user_found = True
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Skipping user {candidate_name} due to error: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if valid_user_found:
|
||||||
|
break
|
||||||
|
|
||||||
|
# If we found a user, check if there's a saved library preference to apply
|
||||||
|
if valid_user_found and preferred_library:
|
||||||
try:
|
try:
|
||||||
# Check this specific user's views (libraries)
|
views_response = self._make_request(f'/Users/{self.user_id}/Views')
|
||||||
views_response = self._make_request(f'/Users/{candidate_id}/Views')
|
|
||||||
|
|
||||||
if views_response:
|
if views_response:
|
||||||
for view in views_response.get('Items', []):
|
for view in views_response.get('Items', []):
|
||||||
# Check if they have a 'music' collection (case-insensitive safe check)
|
|
||||||
collection_type = (view.get('CollectionType') or '').lower()
|
collection_type = (view.get('CollectionType') or '').lower()
|
||||||
|
if collection_type == 'music' and view.get('Name') == preferred_library:
|
||||||
if collection_type == 'music':
|
|
||||||
# Found a winner! Set the class variables.
|
|
||||||
self.user_id = candidate_id
|
|
||||||
self.music_library_id = view['Id']
|
self.music_library_id = view['Id']
|
||||||
logger.info(f"Using user: {candidate_name} (Music Library: {view.get('Name')})")
|
logger.info(f"Applied saved library preference: {preferred_library}")
|
||||||
valid_user_found = True
|
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# If this user fails (e.g. permission error), just log it and try the next user
|
logger.debug(f"Could not apply library preference: {e}")
|
||||||
logger.debug(f"Skipping user {candidate_name} due to error: {e}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# If we found a valid user, stop looping
|
|
||||||
if valid_user_found:
|
|
||||||
break
|
|
||||||
|
|
||||||
if not valid_user_found:
|
if not valid_user_found:
|
||||||
logger.error("Connected to Jellyfin, but could not find any user with access to a Music library")
|
logger.error("Connected to Jellyfin, but could not find any user with access to a Music library")
|
||||||
|
|
@ -321,6 +359,87 @@ class JellyfinClient:
|
||||||
logger.error(f"Error setting music library: {e}")
|
logger.error(f"Error setting music library: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_available_users(self) -> List[Dict[str, str]]:
|
||||||
|
"""Get list of users that have music libraries"""
|
||||||
|
if not self.ensure_connection():
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
users_response = self._make_request('/Users')
|
||||||
|
if not users_response:
|
||||||
|
return []
|
||||||
|
|
||||||
|
users_with_music = []
|
||||||
|
for user in users_response:
|
||||||
|
candidate_id = user['Id']
|
||||||
|
candidate_name = user.get('Name', 'Unknown')
|
||||||
|
|
||||||
|
try:
|
||||||
|
views_response = self._make_request(f'/Users/{candidate_id}/Views')
|
||||||
|
if views_response:
|
||||||
|
for view in views_response.get('Items', []):
|
||||||
|
collection_type = (view.get('CollectionType') or '').lower()
|
||||||
|
if collection_type == 'music':
|
||||||
|
users_with_music.append({
|
||||||
|
'id': candidate_id,
|
||||||
|
'name': candidate_name
|
||||||
|
})
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Skipping user {candidate_name} during enumeration: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.debug(f"Found {len(users_with_music)} users with music libraries")
|
||||||
|
return users_with_music
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting available users: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def set_user_by_name(self, username: str) -> bool:
|
||||||
|
"""Set the active user by name, re-discover their music library, and save preference"""
|
||||||
|
if not self.ensure_connection():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
users_response = self._make_request('/Users')
|
||||||
|
if not users_response:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for user in users_response:
|
||||||
|
if user.get('Name') == username:
|
||||||
|
candidate_id = user['Id']
|
||||||
|
|
||||||
|
# Verify this user has a music library
|
||||||
|
views_response = self._make_request(f'/Users/{candidate_id}/Views')
|
||||||
|
if not views_response:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for view in views_response.get('Items', []):
|
||||||
|
collection_type = (view.get('CollectionType') or '').lower()
|
||||||
|
if collection_type == 'music':
|
||||||
|
self.user_id = candidate_id
|
||||||
|
self.music_library_id = view['Id']
|
||||||
|
logger.info(f"Switched to user: {username} (Music Library: {view.get('Name')})")
|
||||||
|
|
||||||
|
# Save preference to database
|
||||||
|
from database.music_database import MusicDatabase
|
||||||
|
db = MusicDatabase()
|
||||||
|
db.set_preference('jellyfin_user', username)
|
||||||
|
|
||||||
|
# Clear caches since we switched users
|
||||||
|
self.clear_cache()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.warning(f"User '{username}' has no music library")
|
||||||
|
return False
|
||||||
|
|
||||||
|
logger.warning(f"User '{username}' not found")
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting user: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def _make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
def _make_request(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Optional[Dict[str, Any]]:
|
||||||
"""Make authenticated request to Jellyfin API"""
|
"""Make authenticated request to Jellyfin API"""
|
||||||
if not self.base_url or not self.api_key:
|
if not self.base_url or not self.api_key:
|
||||||
|
|
|
||||||
|
|
@ -2617,6 +2617,57 @@ def select_plex_music_library():
|
||||||
logger.error(f"Error setting Plex music library: {e}")
|
logger.error(f"Error setting Plex music library: {e}")
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/jellyfin/users', methods=['GET'])
|
||||||
|
def get_jellyfin_users():
|
||||||
|
"""Get list of Jellyfin users that have music libraries"""
|
||||||
|
try:
|
||||||
|
users = jellyfin_client.get_available_users()
|
||||||
|
|
||||||
|
# Get currently selected user
|
||||||
|
from database.music_database import MusicDatabase
|
||||||
|
db = MusicDatabase()
|
||||||
|
selected_user = db.get_preference('jellyfin_user')
|
||||||
|
|
||||||
|
# Determine the current user name from user_id
|
||||||
|
current_user = None
|
||||||
|
if jellyfin_client.user_id:
|
||||||
|
for u in users:
|
||||||
|
if u['id'] == jellyfin_client.user_id:
|
||||||
|
current_user = u['name']
|
||||||
|
break
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"users": users,
|
||||||
|
"selected": selected_user,
|
||||||
|
"current": current_user
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error getting Jellyfin users: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/jellyfin/select-user', methods=['POST'])
|
||||||
|
def select_jellyfin_user():
|
||||||
|
"""Set the active Jellyfin user"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
username = data.get('username')
|
||||||
|
|
||||||
|
if not username:
|
||||||
|
return jsonify({"success": False, "error": "No username provided"}), 400
|
||||||
|
|
||||||
|
success = jellyfin_client.set_user_by_name(username)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
add_activity_item("👤", "User Selected", f"Jellyfin user set to: {username}", "Now")
|
||||||
|
return jsonify({"success": True, "message": f"User set to: {username}"})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "error": f"User '{username}' not found or has no music library"}), 404
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error setting Jellyfin user: {e}")
|
||||||
|
return jsonify({"success": False, "error": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/jellyfin/music-libraries', methods=['GET'])
|
@app.route('/api/jellyfin/music-libraries', methods=['GET'])
|
||||||
def get_jellyfin_music_libraries():
|
def get_jellyfin_music_libraries():
|
||||||
"""Get list of all available music libraries from Jellyfin"""
|
"""Get list of all available music libraries from Jellyfin"""
|
||||||
|
|
|
||||||
|
|
@ -2754,6 +2754,15 @@
|
||||||
<button class="test-button" onclick="testConnection('jellyfin')">Test</button>
|
<button class="test-button" onclick="testConnection('jellyfin')">Test</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="jellyfin-user-selector-container"
|
||||||
|
style="display: none;">
|
||||||
|
<label>Jellyfin User</label>
|
||||||
|
<select id="jellyfin-user" onchange="selectJellyfinUser()">
|
||||||
|
<option value="">Select User</option>
|
||||||
|
</select>
|
||||||
|
<small>Select which user's music library to scan</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" id="jellyfin-library-selector-container"
|
<div class="form-group" id="jellyfin-library-selector-container"
|
||||||
style="display: none;">
|
style="display: none;">
|
||||||
<label>Music Library</label>
|
<label>Music Library</label>
|
||||||
|
|
|
||||||
|
|
@ -1712,8 +1712,9 @@ async function loadSettingsData() {
|
||||||
loadPlexMusicLibraries();
|
loadPlexMusicLibraries();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Jellyfin music libraries if Jellyfin is the active server
|
// Load Jellyfin users and music libraries if Jellyfin is the active server
|
||||||
if (activeServer === 'jellyfin') {
|
if (activeServer === 'jellyfin') {
|
||||||
|
loadJellyfinUsers();
|
||||||
loadJellyfinMusicLibraries();
|
loadJellyfinMusicLibraries();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1847,8 +1848,9 @@ function toggleServer(serverType) {
|
||||||
loadPlexMusicLibraries();
|
loadPlexMusicLibraries();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load Jellyfin music libraries when switching to Jellyfin
|
// Load Jellyfin users and music libraries when switching to Jellyfin
|
||||||
if (serverType === 'jellyfin') {
|
if (serverType === 'jellyfin') {
|
||||||
|
loadJellyfinUsers();
|
||||||
loadJellyfinMusicLibraries();
|
loadJellyfinMusicLibraries();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2233,6 +2235,7 @@ async function testConnection(service) {
|
||||||
if (service === 'plex') {
|
if (service === 'plex') {
|
||||||
loadPlexMusicLibraries();
|
loadPlexMusicLibraries();
|
||||||
} else if (service === 'jellyfin') {
|
} else if (service === 'jellyfin') {
|
||||||
|
loadJellyfinUsers();
|
||||||
loadJellyfinMusicLibraries();
|
loadJellyfinMusicLibraries();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -30445,6 +30448,79 @@ async function selectPlexLibrary() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============ Jellyfin User Selection ============
|
||||||
|
|
||||||
|
async function loadJellyfinUsers() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/jellyfin/users');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success && data.users && data.users.length > 0) {
|
||||||
|
const selector = document.getElementById('jellyfin-user');
|
||||||
|
const container = document.getElementById('jellyfin-user-selector-container');
|
||||||
|
|
||||||
|
// Clear existing options
|
||||||
|
selector.innerHTML = '';
|
||||||
|
|
||||||
|
// Add options for each user
|
||||||
|
data.users.forEach(user => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = user.name;
|
||||||
|
option.textContent = user.name;
|
||||||
|
|
||||||
|
// Mark the currently selected user
|
||||||
|
if (user.name === data.current || user.name === data.selected) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
selector.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the container
|
||||||
|
container.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
// Hide if no users found or not connected
|
||||||
|
document.getElementById('jellyfin-user-selector-container').style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading Jellyfin users:', error);
|
||||||
|
document.getElementById('jellyfin-user-selector-container').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectJellyfinUser() {
|
||||||
|
const selector = document.getElementById('jellyfin-user');
|
||||||
|
const selectedUser = selector.value;
|
||||||
|
|
||||||
|
if (!selectedUser) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/jellyfin/select-user', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: selectedUser
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
console.log(`Jellyfin user switched to: ${selectedUser}`);
|
||||||
|
// Refresh library dropdown for the new user
|
||||||
|
loadJellyfinMusicLibraries();
|
||||||
|
} else {
|
||||||
|
console.error('Failed to switch user:', data.error);
|
||||||
|
alert(`Failed to switch user: ${data.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error selecting Jellyfin user:', error);
|
||||||
|
alert('Error selecting user. Please try again.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============ Jellyfin Music Library Selection ============
|
// ============ Jellyfin Music Library Selection ============
|
||||||
|
|
||||||
async function loadJellyfinMusicLibraries() {
|
async function loadJellyfinMusicLibraries() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue