This commit is contained in:
Admin9705
2026-02-20 10:55:55 -05:00
parent 3b84f9a387
commit fe5083ca17
8 changed files with 221 additions and 13 deletions

View File

@@ -646,6 +646,100 @@
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.2);
}
/* ===== SIDEBAR USER PROFILE ===== */
.sidebar-user-profile {
width: 100%;
margin-bottom: 6px;
}
.sidebar-user-link {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 8px;
text-decoration: none;
color: inherit;
transition: background 0.15s ease;
}
.sidebar-user-link:hover {
background: rgba(99, 102, 241, 0.08);
text-decoration: none;
}
.sidebar-user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
flex-shrink: 0;
overflow: hidden;
}
.sidebar-user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.sidebar-avatar-fallback {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.4), rgba(139, 92, 246, 0.4));
border: 1px solid rgba(99, 102, 241, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.85rem;
font-weight: 700;
color: #e2e8f0;
}
.sidebar-user-info {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.sidebar-user-name {
font-size: 0.8rem;
font-weight: 600;
color: #e2e8f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sidebar-user-role {
font-size: 0.65rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #64748b;
}
.sidebar-user-role-owner {
color: #eab308;
}
.sidebar-user-role-user {
color: #818cf8;
}
.sidebar-user-settings-icon {
font-size: 0.75rem;
color: #475569;
flex-shrink: 0;
transition: color 0.15s ease;
}
.sidebar-user-link:hover .sidebar-user-settings-icon {
color: #94a3b8;
}
/* ===== SIDEBAR FOOTER ===== */
.sidebar-footer {
flex-shrink: 0;

View File

@@ -326,6 +326,16 @@
</nav>
<div class="sidebar-footer">
<div class="sidebar-user-profile" id="sidebar-user-profile">
<a href="#user" class="sidebar-user-link">
<div class="sidebar-user-avatar" id="sidebar-user-avatar"></div>
<div class="sidebar-user-info">
<span class="sidebar-user-name" id="sidebar-user-name">Loading...</span>
<span class="sidebar-user-role" id="sidebar-user-role"></span>
</div>
<i class="fas fa-cog sidebar-user-settings-icon"></i>
</a>
</div>
<div class="sidebar-social-group">
<a href="https://discord.com/invite/PGJJjR5Cww" target="_blank" rel="noopener" class="sidebar-social-btn discord-btn" title="Join Discord" aria-label="Discord">
<i class="fab fa-discord"></i>
@@ -700,6 +710,39 @@ function setActiveNavItem() {
setTimeout(updateSidebarVersion, 2000);
})();
// ── Sidebar User Profile ─────────────────────────────────────
(function() {
function loadSidebarUser() {
fetch('./api/user/me', { credentials: 'include', cache: 'no-store' })
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(data) {
if (!data || !data.username) return;
var nameEl = document.getElementById('sidebar-user-name');
var roleEl = document.getElementById('sidebar-user-role');
var avatarEl = document.getElementById('sidebar-user-avatar');
if (nameEl) nameEl.textContent = data.username;
if (roleEl) {
roleEl.textContent = data.role === 'owner' ? 'Owner' : 'User';
roleEl.className = 'sidebar-user-role sidebar-user-role-' + data.role;
}
if (avatarEl) {
var initial = (data.username || '?').charAt(0).toUpperCase();
if (data.avatar_url) {
avatarEl.innerHTML = '<img src="' + data.avatar_url + '" alt="" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><span class="sidebar-avatar-fallback" style="display:none;">' + initial + '</span>';
} else {
avatarEl.innerHTML = '<span class="sidebar-avatar-fallback">' + initial + '</span>';
}
}
})
.catch(function() {});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadSidebarUser);
} else {
loadSidebarUser();
}
})();
// ── Init ─────────────────────────────────────────────────────
window.addEventListener('hashchange', setActiveNavItem);
if (document.readyState === 'loading') {

View File

@@ -1,9 +1,6 @@
"""
Swaparr app module for Huntarr
Contains functionality for handling stalled downloads in Starr apps
Based on the original Swaparr project by ThijmenGThN:
https://github.com/ThijmenGThN/swaparr/releases/tag/0.10.0
"""
# Add necessary imports for get_configured_instances

View File

@@ -1,6 +1,5 @@
"""
Enhanced implementation of the swaparr functionality to detect and remove stalled downloads in Starr apps.
Based on the functionality provided by https://github.com/ThijmenGThN/swaparr/releases/tag/0.10.0
Improvements in this version:
- Better statistics tracking and reporting

View File

@@ -560,6 +560,50 @@ def get_user_info_route():
two_fa_status = is_2fa_enabled(username) # This function should now be defined via import
return jsonify({"username": username, "is_2fa_enabled": two_fa_status})
@common_bp.route('/api/user/me', methods=['GET'])
def get_current_user_profile():
"""Get current user's profile for sidebar display (avatar, username, role)."""
username = get_user_for_request()
if not username:
return jsonify({"error": "Not authenticated"}), 401
try:
from src.primary.utils.database import get_database
db = get_database()
role = 'owner'
avatar_url = None
# Check requestarr_users first (has role info)
req_user = db.get_requestarr_user_by_username(username)
if req_user:
role = req_user.get('role', 'user')
avatar_url = req_user.get('avatar_url')
# Fall back to plex_user_data thumb in requestarr record
if not avatar_url and isinstance(req_user.get('plex_user_data'), dict):
avatar_url = req_user['plex_user_data'].get('thumb')
# For owner (or if no avatar found), also check main users table plex data
if not avatar_url:
main_user = db.get_user_by_username(username) or db.get_first_user()
if main_user:
plex_data = main_user.get('plex_user_data')
if isinstance(plex_data, str):
import json as _json
try:
plex_data = _json.loads(plex_data)
except Exception:
plex_data = None
if isinstance(plex_data, dict):
avatar_url = plex_data.get('thumb')
return jsonify({
'username': username,
'role': role,
'avatar_url': avatar_url,
})
except Exception as e:
logger.error(f"Error getting user profile: {e}")
return jsonify({'username': username, 'role': 'owner', 'avatar_url': None})
@common_bp.route('/api/user/change-username', methods=['POST'])
def change_username_route():
# Get username handling bypass modes

View File

@@ -75,11 +75,11 @@ def _sanitize_user(user_dict):
'permissions': user_dict.get('permissions', '{}'),
'created_at': user_dict.get('created_at'),
'plex_user_data': user_dict.get('plex_user_data'),
'avatar_url': None,
'avatar_url': user_dict.get('avatar_url') or None,
'request_count': user_dict.get('request_count', 0),
}
# Extract avatar from plex data if available
if isinstance(safe['plex_user_data'], dict):
# Extract avatar from plex data if not already set in avatar_url column
if not safe['avatar_url'] and isinstance(safe['plex_user_data'], dict):
safe['avatar_url'] = safe['plex_user_data'].get('thumb')
# Parse permissions JSON
if isinstance(safe['permissions'], str):
@@ -265,13 +265,24 @@ def get_current_user_info():
req_user = db.get_requestarr_user_by_username(user.get('username'))
if req_user:
return jsonify({'user': _sanitize_user(req_user)})
# Return basic info from the main users table
# Return basic info from the main users table (owner fallback)
import json as _json
avatar_url = None
plex_data = user.get('plex_user_data')
if isinstance(plex_data, str):
try:
plex_data = _json.loads(plex_data)
except Exception:
plex_data = None
if isinstance(plex_data, dict):
avatar_url = plex_data.get('thumb')
return jsonify({'user': {
'id': user.get('id'),
'username': user.get('username'),
'role': user.get('role', 'owner'),
'permissions': DEFAULT_PERMISSIONS.get('owner', {}),
'created_at': user.get('created_at'),
'avatar_url': avatar_url,
}})
@@ -447,6 +458,10 @@ def import_plex_users():
plex_user_data=plex_data
)
if success:
# Store avatar URL directly in the avatar_url column
new_user = db.get_requestarr_user_by_username(username)
if new_user and plex_user['thumb']:
db.update_requestarr_user(new_user['id'], {'avatar_url': plex_user['thumb']})
imported.append(username)
else:
skipped.append({'id': fid, 'username': username, 'reason': 'Creation failed'})

View File

@@ -314,13 +314,29 @@ class UsersMixin:
return False
def ensure_owner_in_requestarr_users(self):
"""Ensure the main owner account exists in requestarr_users table."""
"""Ensure the main owner account exists in requestarr_users table.
Also syncs the owner's Plex avatar if available.
"""
try:
owner = self.get_first_user()
if not owner:
return
# Extract avatar from owner's plex_user_data
avatar_url = None
plex_data = owner.get('plex_user_data')
if isinstance(plex_data, str):
try:
plex_data = json.loads(plex_data)
except Exception:
plex_data = None
if isinstance(plex_data, dict):
avatar_url = plex_data.get('thumb')
existing = self.get_requestarr_user_by_username(owner['username'])
if existing:
# Sync avatar if owner has Plex linked but requestarr record lacks avatar
if avatar_url and not existing.get('avatar_url'):
self.update_requestarr_user(existing['id'], {'avatar_url': avatar_url})
return
owner_perms = json.dumps({
'request_movies': True, 'request_tv': True,
@@ -330,9 +346,9 @@ class UsersMixin:
})
with self.get_connection() as conn:
conn.execute('''
INSERT OR IGNORE INTO requestarr_users (username, password, email, role, permissions, created_at, updated_at)
VALUES (?, ?, '', 'owner', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
''', (owner['username'], owner['password'], owner_perms))
INSERT OR IGNORE INTO requestarr_users (username, password, email, role, permissions, avatar_url, created_at, updated_at)
VALUES (?, ?, '', 'owner', ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
''', (owner['username'], owner['password'], owner_perms, avatar_url))
conn.commit()
logger.info(f"Synced owner '{owner['username']}' into requestarr_users")
except Exception as e:

View File

@@ -370,7 +370,7 @@ def _set_response_headers(response):
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; "
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; "
"font-src 'self' https://cdnjs.cloudflare.com; "
"img-src 'self' data: https://image.tmdb.org https://artworks.thetvdb.com https://*.plex.direct https://github.com https://avatars.githubusercontent.com; "
"img-src 'self' data: https://image.tmdb.org https://artworks.thetvdb.com https://*.plex.direct https://plex.tv https://assets.plex.tv https://github.com https://avatars.githubusercontent.com; "
"connect-src 'self' https://api.github.com https://api.themoviedb.org; "
f"frame-ancestors {fa};"
)