mirror of
https://github.com/LLukas22/Jellyswarrm.git
synced 2025-12-23 22:47:47 -05:00
cleanup a bit more
This commit is contained in:
@@ -12,6 +12,7 @@ use crate::{
|
||||
pub enum SyncStatus {
|
||||
Created,
|
||||
AlreadyExists,
|
||||
ExistsWithDifferentPassword,
|
||||
Failed,
|
||||
Skipped,
|
||||
Deleted,
|
||||
@@ -145,42 +146,65 @@ impl FederatedUserService {
|
||||
.await
|
||||
{
|
||||
Ok((remote_user_id, created)) => {
|
||||
let status = if created {
|
||||
SyncStatus::Created
|
||||
let (status, should_map) = if created {
|
||||
(SyncStatus::Created, true)
|
||||
} else {
|
||||
SyncStatus::AlreadyExists
|
||||
// User exists. Check if password matches.
|
||||
match self
|
||||
.check_user_password(server.url.as_ref(), username, password)
|
||||
.await
|
||||
{
|
||||
Ok(true) => (SyncStatus::AlreadyExists, true),
|
||||
Ok(false) => (SyncStatus::ExistsWithDifferentPassword, false),
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to check password for existing user {} on {}: {}",
|
||||
username, server.name, e
|
||||
);
|
||||
// Assume mismatch or failure, don't map to be safe
|
||||
(SyncStatus::ExistsWithDifferentPassword, false)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Synced user {} to server {} (Remote ID: {}, Created: {})",
|
||||
username, server.name, remote_user_id, created
|
||||
"Synced user {} to server {} (Remote ID: {}, Status: {:?})",
|
||||
username, server.name, remote_user_id, status
|
||||
);
|
||||
|
||||
if let Err(e) = self
|
||||
.user_authorization
|
||||
.add_server_mapping(
|
||||
user_id,
|
||||
server.url.as_str(),
|
||||
username,
|
||||
password,
|
||||
Some(password), // Encrypt with their own password so they can use it
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to create local mapping for synced user on server {}: {}",
|
||||
server.name, e
|
||||
);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Failed to save local mapping: {}", e)),
|
||||
});
|
||||
if should_map {
|
||||
if let Err(e) = self
|
||||
.user_authorization
|
||||
.add_server_mapping(
|
||||
user_id,
|
||||
server.url.as_str(),
|
||||
username,
|
||||
password,
|
||||
Some(password), // Encrypt with their own password so they can use it
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to create local mapping for synced user on server {}: {}",
|
||||
server.name, e
|
||||
);
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status: SyncStatus::Failed,
|
||||
message: Some(format!("Failed to save local mapping: {}", e)),
|
||||
});
|
||||
} else {
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status,
|
||||
message: None,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
results.push(ServerSyncResult {
|
||||
server_name: server.name.clone(),
|
||||
status,
|
||||
message: None,
|
||||
message: Some("User exists with different password".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -321,6 +345,45 @@ impl FederatedUserService {
|
||||
results
|
||||
}
|
||||
|
||||
async fn check_user_password(
|
||||
&self,
|
||||
server_url: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<bool, anyhow::Error> {
|
||||
let auth_url = format!(
|
||||
"{}/Users/AuthenticateByName",
|
||||
server_url.trim_end_matches('/')
|
||||
);
|
||||
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
let response = self
|
||||
.reqwest_client
|
||||
.post(&auth_url)
|
||||
.header("Authorization", auth_header)
|
||||
.json(&serde_json::json!({
|
||||
"Username": username,
|
||||
"Pw": password
|
||||
}))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(true)
|
||||
} else if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Authentication check failed: {}",
|
||||
response.status()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn authenticate_as_admin(
|
||||
&self,
|
||||
server_url: &str,
|
||||
|
||||
135
crates/jellyswarrm-proxy/src/ui/resources/custom.css
Normal file
135
crates/jellyswarrm-proxy/src/ui/resources/custom.css
Normal file
@@ -0,0 +1,135 @@
|
||||
/* Global Styles for Jellyswarrm */
|
||||
|
||||
/* --- Components --- */
|
||||
|
||||
/* Icon Buttons */
|
||||
.icon-btn {
|
||||
--_size: 1.1rem;
|
||||
font-size: var(--_size);
|
||||
line-height: 1;
|
||||
padding: .5rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--pico-muted-border-color, #555);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
transition: all 0.2s;
|
||||
color: var(--pico-color);
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: var(--pico-card-background-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.icon-btn.danger {
|
||||
color: var(--pico-del-color, #e55353);
|
||||
border-color: var(--pico-del-color, #e55353);
|
||||
}
|
||||
|
||||
.icon-btn.danger:hover {
|
||||
background: rgba(229, 83, 83, 0.1);
|
||||
}
|
||||
|
||||
/* Badges & Chips */
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: .25em .55em;
|
||||
font-size: .75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
border-radius: 1rem;
|
||||
color: #fff;
|
||||
background: var(--pico-primary-background);
|
||||
}
|
||||
|
||||
.badge.muted, .status-chip {
|
||||
background: var(--pico-muted-border-color, rgba(127, 127, 127, .15));
|
||||
color: var(--pico-color);
|
||||
}
|
||||
|
||||
.badge.success { background-color: #2e7d32; color: white; }
|
||||
.badge.warning { background-color: #ffc107; color: black; }
|
||||
.badge.danger { background-color: #dc3545; color: white; }
|
||||
.badge.secondary { background-color: #6c757d; color: white; }
|
||||
|
||||
/* Status Container */
|
||||
.status-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Filter Bar */
|
||||
.filter-bar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* User Key */
|
||||
.user-key {
|
||||
font-size: .6rem;
|
||||
opacity: .6;
|
||||
word-break: break-all;
|
||||
padding-top: .25em;
|
||||
}
|
||||
|
||||
/* Danger Text/Icon */
|
||||
.text-danger, .danger {
|
||||
color: var(--pico-del-color, #d9534f);
|
||||
}
|
||||
i.danger {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.empty-state {
|
||||
padding: 1rem;
|
||||
border: 2px dashed var(--pico-border-color, #ccc);
|
||||
border-radius: .6rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Sync Report */
|
||||
.sync-report {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--pico-card-background-color);
|
||||
border: 1px solid var(--pico-card-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
}
|
||||
.sync-report ul {
|
||||
margin-bottom: 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
/* --- Animations --- */
|
||||
.htmx-added {
|
||||
animation: fadeInSoft 160ms ease-out, highlightSoft 900ms ease-out;
|
||||
}
|
||||
.htmx-swapping {
|
||||
animation: fadeOutSoft 140ms ease-in forwards;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes fadeInSoft {
|
||||
from { opacity: 0; transform: translateY(2px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeOutSoft {
|
||||
from { opacity: 1; transform: translateY(0); }
|
||||
to { opacity: 0; transform: translateY(-2px); }
|
||||
}
|
||||
|
||||
@keyframes highlightSoft {
|
||||
0% { background: var(--pico-mark-background-color, #fff8d9); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.htmx-added, .htmx-swapping { animation: none; }
|
||||
}
|
||||
@@ -1,13 +1,3 @@
|
||||
<style>
|
||||
/* Lightweight shared styles (can be moved to global if reused) */
|
||||
.icon-btn {--_size:1.1rem; font-size:var(--_size); line-height:1; padding:.5rem; display:inline-flex; align-items:center; justify-content:center; border:1px solid var(--muted-border,#555); background:transparent; cursor:pointer; border-radius: 0.25rem; transition: all 0.2s;}
|
||||
.icon-btn:hover {background:rgba(255,255,255,.1); transform: translateY(-1px);}
|
||||
.icon-btn.danger {color:var(--del,#e55353); border-color:var(--del,#e55353);}
|
||||
.icon-btn.danger:hover {background:rgba(229, 83, 83, 0.1);}
|
||||
.status-chip {font-size:.75rem; padding:.25rem .75rem; border-radius:1rem; background:var(--pill-bg,rgba(127,127,127,.15)); display:inline-flex; align-items: center; justify-content: center; min-width:80px; font-weight: 500;}
|
||||
.status-container { display: flex; align-items: center; gap: 0.75rem; }
|
||||
</style>
|
||||
|
||||
{% if servers.is_empty() %}
|
||||
<article>
|
||||
<header><h4>No servers configured</h4></header>
|
||||
@@ -47,7 +37,7 @@
|
||||
<span style="color: #666;">Checking...</span>
|
||||
</span>
|
||||
{% if item.has_admin %}
|
||||
<span class="status-chip" style="background-color: #2e7d32; color: white;">
|
||||
<span class="badge success">
|
||||
<i class="fas fa-shield-alt" style="margin-right: 0.3rem; font-size: 0.7rem;"></i> Federated
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,84 +1,3 @@
|
||||
<style>
|
||||
/* Minimal additions on top of Pico defaults */
|
||||
.filter-bar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: .25em .55em;
|
||||
font-size: .65em;
|
||||
background: var(--pico-primary, #0b6efd);
|
||||
color: #fff;
|
||||
border-radius: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.user-key {
|
||||
font-size: .6rem;
|
||||
opacity: .6;
|
||||
word-break: break-all;
|
||||
padding-top: .25em;
|
||||
}
|
||||
|
||||
.pill {
|
||||
display: inline-block;
|
||||
padding: .25em .7em;
|
||||
background: var(--pico-muted-border-color, rgba(127, 127, 127, .15));
|
||||
border-radius: 1rem;
|
||||
font-size: .7em;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--pico-del-color, #d9534f);
|
||||
border-color: var(--pico-del-color, #d9534f);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 1rem;
|
||||
border: 2px dashed var(--pico-border-color, #ccc);
|
||||
border-radius: .6rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Softer animations */
|
||||
.htmx-added {
|
||||
animation: fadeInSoft 160ms ease-out, highlightSoft 900ms ease-out;
|
||||
}
|
||||
.htmx-swapping {
|
||||
animation: fadeOutSoft 140ms ease-in forwards;
|
||||
pointer-events:none;
|
||||
}
|
||||
@keyframes fadeInSoft {
|
||||
from { opacity:0; transform:translateY(2px); }
|
||||
to { opacity:1; transform:translateY(0); }
|
||||
}
|
||||
@keyframes fadeOutSoft {
|
||||
from { opacity:1; transform:translateY(0); }
|
||||
to { opacity:0; transform:translateY(-2px); }
|
||||
}
|
||||
@keyframes highlightSoft {
|
||||
0% { background:#fff8d9; }
|
||||
100% { background:transparent; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.htmx-added, .htmx-swapping { animation:none; }
|
||||
}
|
||||
|
||||
.sync-report {
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--pico-card-background-color);
|
||||
border: 1px solid var(--pico-card-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
}
|
||||
.sync-report ul {
|
||||
margin-bottom: 0;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% if let Some(report) = sync_report %}
|
||||
<div class="sync-report">
|
||||
<strong>Federated Sync Results:</strong>
|
||||
@@ -88,17 +7,19 @@
|
||||
{{ result.server_name }}:
|
||||
{% match result.status %}
|
||||
{% when crate::federated_users::SyncStatus::Created %}
|
||||
<span class="badge" style="background-color: #28a745;">Created</span>
|
||||
<span class="badge success">Created</span>
|
||||
{% when crate::federated_users::SyncStatus::AlreadyExists %}
|
||||
<span class="badge" style="background-color: #ffc107; color: black;">Exists</span>
|
||||
<span class="badge warning">Exists</span>
|
||||
{% when crate::federated_users::SyncStatus::ExistsWithDifferentPassword %}
|
||||
<span class="badge danger">Exists (Password Mismatch)</span>
|
||||
{% when crate::federated_users::SyncStatus::Failed %}
|
||||
<span class="badge" style="background-color: #dc3545;">Failed</span>
|
||||
<span class="badge danger">Failed</span>
|
||||
{% when crate::federated_users::SyncStatus::Skipped %}
|
||||
<span class="badge" style="background-color: #6c757d;">Skipped</span>
|
||||
<span class="badge secondary">Skipped</span>
|
||||
{% when crate::federated_users::SyncStatus::Deleted %}
|
||||
<span class="badge" style="background-color: #dc3545;">Deleted</span>
|
||||
<span class="badge danger">Deleted</span>
|
||||
{% when crate::federated_users::SyncStatus::NotFound %}
|
||||
<span class="badge" style="background-color: #6c757d;">Not Found</span>
|
||||
<span class="badge secondary">Not Found</span>
|
||||
{% endmatch %}
|
||||
{% if let Some(msg) = result.message %}
|
||||
<small>({{ msg }})</small>
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
<!-- Font Awesome -->
|
||||
<link rel="stylesheet" href="/{{ ui_route }}/resources/fontawesome/css/all.min.css">
|
||||
|
||||
<!-- Custom Styles -->
|
||||
<link rel="stylesheet" href="/{{ ui_route }}/resources/custom.css">
|
||||
|
||||
<!-- HTMX -->
|
||||
<script src="/{{ ui_route }}/resources/htmx.min.js"></script>
|
||||
|
||||
|
||||
@@ -23,6 +23,20 @@ struct MediaFoldersResponse {
|
||||
items: Vec<MediaFolder>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthResponse {
|
||||
#[serde(rename = "AccessToken")]
|
||||
access_token: String,
|
||||
#[serde(rename = "User")]
|
||||
user: JellyfinUser,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JellyfinUser {
|
||||
#[serde(rename = "Id")]
|
||||
id: String,
|
||||
}
|
||||
|
||||
pub struct ServerLibraries {
|
||||
pub server_name: String,
|
||||
pub libraries: Vec<MediaFolder>,
|
||||
@@ -67,18 +81,24 @@ pub async fn get_user_media(
|
||||
};
|
||||
|
||||
for server in servers {
|
||||
let mut libraries = Vec::new();
|
||||
let mut error_msg = None;
|
||||
|
||||
// Find session for this server
|
||||
let session = sessions
|
||||
.iter()
|
||||
.filter(|(_, s)| s.id == server.id)
|
||||
.max_by_key(|(auth, _)| auth.updated_at);
|
||||
|
||||
if let Some((auth, _)) = session {
|
||||
let mut token = session.map(|(auth, _)| auth.jellyfin_token.clone());
|
||||
|
||||
// 1. Try to use existing token if available
|
||||
if let Some(t) = &token {
|
||||
let url = join_server_url(&server.url, "/Library/MediaFolders");
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\", Token=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
auth.jellyfin_token
|
||||
t
|
||||
);
|
||||
|
||||
match client
|
||||
@@ -90,50 +110,121 @@ pub async fn get_user_media(
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<MediaFoldersResponse>().await {
|
||||
Ok(folders) => {
|
||||
server_libraries.push(ServerLibraries {
|
||||
server_name: server.name.clone(),
|
||||
libraries: folders.items,
|
||||
error: None,
|
||||
});
|
||||
libraries = folders.items;
|
||||
}
|
||||
Err(e) => {
|
||||
server_libraries.push(ServerLibraries {
|
||||
server_name: server.name.clone(),
|
||||
libraries: Vec::new(),
|
||||
error: Some(format!("Failed to parse: {}", e)),
|
||||
});
|
||||
error_msg = Some(format!("Failed to parse: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(resp) if resp.status() == StatusCode::FORBIDDEN || resp.status() == StatusCode::UNAUTHORIZED => {
|
||||
// Token expired, clear it to trigger re-login
|
||||
token = None;
|
||||
}
|
||||
Ok(resp) => {
|
||||
let error_msg = if resp.status() == StatusCode::FORBIDDEN
|
||||
|| resp.status() == StatusCode::UNAUTHORIZED
|
||||
{
|
||||
"Session expired, please reconnect".to_string()
|
||||
} else {
|
||||
format!("HTTP {}", resp.status())
|
||||
};
|
||||
server_libraries.push(ServerLibraries {
|
||||
server_name: server.name.clone(),
|
||||
libraries: Vec::new(),
|
||||
error: Some(error_msg),
|
||||
});
|
||||
error_msg = Some(format!("HTTP {}", resp.status()));
|
||||
}
|
||||
Err(e) => {
|
||||
server_libraries.push(ServerLibraries {
|
||||
server_name: server.name.clone(),
|
||||
libraries: Vec::new(),
|
||||
error: Some(format!("Network error: {}", e)),
|
||||
});
|
||||
error_msg = Some(format!("Network error: {}", e));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
server_libraries.push(ServerLibraries {
|
||||
server_name: server.name.clone(),
|
||||
libraries: Vec::new(),
|
||||
error: Some("Not connected".to_string()),
|
||||
});
|
||||
}
|
||||
|
||||
// 2. If no token or expired, try to login using mapping
|
||||
if token.is_none() && (libraries.is_empty() && error_msg.is_none() || error_msg.as_deref() == Some("HTTP 401") || error_msg.as_deref() == Some("HTTP 403")) {
|
||||
// Clear previous error if we are retrying
|
||||
error_msg = None;
|
||||
|
||||
match state.user_authorization.get_server_mapping(&user.id, &server.url.as_str()).await {
|
||||
Ok(Some(mapping)) => {
|
||||
// Decrypt password
|
||||
let config = state.config.read().await;
|
||||
let admin_password = &config.password;
|
||||
|
||||
let decrypted_password = state.user_authorization.decrypt_server_mapping_password(
|
||||
&mapping,
|
||||
&user.password,
|
||||
admin_password
|
||||
);
|
||||
|
||||
// Perform login
|
||||
let auth_url = join_server_url(&server.url, "/Users/AuthenticateByName");
|
||||
let body = serde_json::json!({
|
||||
"Username": mapping.mapped_username,
|
||||
"Pw": decrypted_password
|
||||
});
|
||||
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
|
||||
match client.post(auth_url.as_str()).header("Authorization", auth_header).json(&body).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<AuthResponse>().await {
|
||||
Ok(auth_resp) => {
|
||||
// Store new session
|
||||
let auth = crate::models::Authorization {
|
||||
client: "Jellyswarrm Proxy".to_string(),
|
||||
device: "Server".to_string(),
|
||||
device_id: "jellyswarrm-proxy".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
token: None,
|
||||
};
|
||||
|
||||
if let Err(e) = state.user_authorization.store_authorization_session(
|
||||
&user.id,
|
||||
server.url.as_str(),
|
||||
&auth,
|
||||
auth_resp.access_token.clone(),
|
||||
auth_resp.user.id,
|
||||
None
|
||||
).await {
|
||||
error!("Failed to store session: {}", e);
|
||||
}
|
||||
|
||||
// Fetch libraries with new token
|
||||
let url = join_server_url(&server.url, "/Library/MediaFolders");
|
||||
let auth_header = format!(
|
||||
"MediaBrowser Client=\"Jellyswarrm Proxy\", Device=\"Server\", DeviceId=\"jellyswarrm-proxy\", Version=\"{}\", Token=\"{}\"",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
auth_resp.access_token
|
||||
);
|
||||
|
||||
match client.get(url.as_str()).header("Authorization", auth_header).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
match resp.json::<MediaFoldersResponse>().await {
|
||||
Ok(folders) => {
|
||||
libraries = folders.items;
|
||||
}
|
||||
Err(e) => error_msg = Some(format!("Failed to parse: {}", e)),
|
||||
}
|
||||
}
|
||||
Ok(resp) => error_msg = Some(format!("HTTP {}", resp.status())),
|
||||
Err(e) => error_msg = Some(format!("Network error: {}", e)),
|
||||
}
|
||||
}
|
||||
Err(e) => error_msg = Some(format!("Login response error: {}", e)),
|
||||
}
|
||||
}
|
||||
Ok(resp) => error_msg = Some(format!("Login failed: HTTP {}", resp.status())),
|
||||
Err(e) => error_msg = Some(format!("Login network error: {}", e)),
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
error_msg = Some("Not connected".to_string());
|
||||
}
|
||||
Err(e) => {
|
||||
error_msg = Some(format!("Database error: {}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server_libraries.push(ServerLibraries {
|
||||
server_name: server.name.clone(),
|
||||
libraries,
|
||||
error: error_msg,
|
||||
});
|
||||
}
|
||||
|
||||
let template = UserMediaTemplate {
|
||||
|
||||
Reference in New Issue
Block a user