mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-04-20 01:06:57 -04:00
Refactor
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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};"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user