diff --git a/src/config.rs b/src/config.rs index 9d46527..9cc1da9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ pub struct AccessConfig { pub smb_url: Option, pub smb_user: Option, pub smb_pass: Option, + pub smb_domain: Option, pub synology_path: Option, } @@ -45,6 +46,7 @@ impl AccessConfig { smb_url, smb_user: env::var(format!("{}_SMB_USER", prefix)).ok(), smb_pass: env::var(format!("{}_SMB_PASS", prefix)).ok(), + smb_domain: env::var(format!("{}_SMB_DOMAIN", prefix)).ok(), synology_path, }) } diff --git a/src/file_access.rs b/src/file_access.rs index ab56e86..79ba7e0 100644 --- a/src/file_access.rs +++ b/src/file_access.rs @@ -71,7 +71,11 @@ impl FileAccess for LocalFileAccess { async fn read_file(&self, filename: &str) -> Result> { let path = self.full_path(filename); - tokio::fs::read(&path).await.map_err(Into::into) + log::debug!("LocalFileAccess reading file: {}", path); + tokio::fs::read(&path).await.map_err(|e| { + log::error!("Failed to read local file {}: {}", path, e); + anyhow::anyhow!(e) + }) } async fn write_file(&self, filename: &str, data: &[u8]) -> Result<()> { @@ -92,6 +96,7 @@ pub struct SmbAccessConfig { pub url: String, pub user: Option, pub password: Option, + pub domain: Option, } pub struct SmbFileAccess { @@ -107,21 +112,29 @@ impl SmbFileAccess { let base_unc = smb_url_to_unc(&self.config.url)?; let client = Client::new(ClientConfig::default()); - if let (Some(user), Some(pass)) = (&self.config.user, &self.config.password) { - client.share_connect(&base_unc, user, pass.clone()).await?; - } else { - client.share_connect(&base_unc, "", "".to_string()).await?; - } + let (user, pass) = match (&self.config.user, &self.config.password) { + (Some(u), Some(p)) => (u.clone(), p.clone()), + _ => { + client.share_connect(&base_unc, "", "".to_string()).await?; + return Ok((client, base_unc)); + } + }; + + // Если задан домен, добавляем его к имени пользователя: DOMAIN\username + let full_user = match &self.config.domain { + Some(d) if !d.is_empty() => format!("{}\\{}", d, user), + _ => user, + }; + + client.share_connect(&base_unc, &full_user, pass).await?; Ok((client, base_unc)) } - fn full_unc_path(&self, base: &UncPath, filename: &str) -> UncPath { - let segments: Vec<&str> = filename.split('/').filter(|s| !s.is_empty()).collect(); - let mut path = base.clone(); - for seg in segments { - path = path.with_path(seg); - } - path + /// Построить полный UNC путь к файлу, добавляя filename к базовому пути (который уже включает подпапки) + fn make_file_unc(base: &UncPath, filename: &str) -> Result { + let base_str = base.to_string(); // \\server\share\path + let full = format!("{}\\{}", base_str.trim_end_matches('\\'), filename); + UncPath::from_str(&full).map_err(|e| anyhow!("Invalid file UNC: {}", e)) } } @@ -135,11 +148,15 @@ impl FileAccess for SmbFileAccess { async fn read_file(&self, filename: &str) -> Result> { let (client, base_unc) = self.connect().await?; - let file_path = self.full_unc_path(&base_unc, filename); + let file_path = Self::make_file_unc(&base_unc, filename)?; + log::info!("SMB reading file: {:?}", file_path); let args = FileCreateArgs::make_open_existing(FileAccessMask::new().with_generic_read(true)); - let resource = client.create_file(&file_path, &args).await?; + let resource = client.create_file(&file_path, &args).await.map_err(|e| { + log::error!("Failed to open SMB file {:?}: {}", file_path, e); + anyhow::anyhow!("Failed to open SMB file: {}", e) + })?; let file = resource.unwrap_file(); let file_size = file.get_len().await? as usize; @@ -153,11 +170,15 @@ impl FileAccess for SmbFileAccess { async fn write_file(&self, filename: &str, data: &[u8]) -> Result<()> { let (client, base_unc) = self.connect().await?; - let file_path = self.full_unc_path(&base_unc, filename); + let file_path = Self::make_file_unc(&base_unc, filename)?; + log::info!("SMB writing file: {:?}", file_path); let args = FileCreateArgs::make_overwrite(FileAttributes::default(), CreateOptions::default()); - let resource = client.create_file(&file_path, &args).await?; + let resource = client.create_file(&file_path, &args).await.map_err(|e| { + log::error!("Failed to create SMB file {:?}: {}", file_path, e); + anyhow::anyhow!("Failed to create SMB file: {}", e) + })?; let remote_file = resource.unwrap_file(); remote_file.write_at(data, 0).await?; diff --git a/src/static/index.html b/src/static/index.html index caf7620..a50ad1a 100644 --- a/src/static/index.html +++ b/src/static/index.html @@ -28,102 +28,86 @@ - -
- - -
- - -
-
- - - - -
- - Ready -
-
- -
- -
-
- - -
-
- - Auto-refresh: 60s - 60s -
-
- -
- - - - - - - - - - - - - - - -
- Output File - - State - - Created - - Updated - - UID -
-
- -

Loading jobs...

-
-
+ +
+ + + + +
+ + Ready
- -
-
- -
- Видео не загружены -
+ +
+ + +
+
+ + +
+
+ + Auto-refresh: 60s + 60s
-
- - + + + @@ -228,19 +212,27 @@ function renderJobs(jobs) { const tbody = document.getElementById('jobsTableBody'); if (jobs.length === 0) { - tbody.innerHTML = '

No jobs found

'; + tbody.innerHTML = '

No jobs found

'; return; } tbody.innerHTML = jobs.map(job => { const stateClass = getStateClass(job.state); const created = formatDateTime(job.created_at); const updated = formatDateTime(job.updated_at); + const previewBtn = job.state === 'finished' + ? `` + : ''; + const moveBtn = job.state === 'finished' + ? `` + : ''; return `
${escapeHtml(job.outfile_name)}
${escapeHtml(job.state)} ${created} ${updated} ${job.uid.substring(0, 10)}... + ${previewBtn} + ${moveBtn} `; }).join(''); } @@ -277,66 +269,98 @@ filteredJobs = allJobs.filter(job => job.outfile_name.toLowerCase().includes(filter) || job.uid.toLowerCase().includes(filter)); sortAndRender(); } - async function generateJobs() { /* ... тот же код ... */ } - async function stopAllJobs() { /* ... тот же код ... */ } - async function cleanupJobs() { /* ... тот же код ... */ } - function setStatus(type, message) { /* ... тот же код ... */ } - function startAutoRefresh() { /* ... тот же код ... */ } - function stopAutoRefresh() { /* ... тот же код ... */ } - function resetCountdown() { /* ... тот же код ... */ } - // Скопируйте оригинальные реализации generateJobs, stopAllJobs, cleanupJobs, setStatus, startAutoRefresh, stopAutoRefresh, resetCountdown (они у вас уже были) - // Я не стал дублировать весь код, чтобы не загромождать ответ. Просто вставьте их из вашего предыдущего файла. + // Оригинальные реализации generateJobs, stopAllJobs, cleanupJobs, setStatus, startAutoRefresh, stopAutoRefresh, resetCountdown + async function generateJobs() { + setStatus('loading', 'Generating jobs...'); + try { + const response = await fetch('/api/generate', { method: 'POST' }); + if (response.ok) { + setStatus('success', 'Job generation started'); + setTimeout(() => refreshJobs(), 5000); + } else { + const text = await response.text(); + setStatus('error', `Error: ${text}`); + } + } catch (err) { + setStatus('error', `Error: ${err}`); + } + } - // ========== НОВЫЙ КОД: Вкладки ========== - document.querySelectorAll('.tab-btn').forEach(btn => { - btn.addEventListener('click', () => { - document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); - document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); - btn.classList.add('active'); - document.getElementById('tab-' + btn.dataset.tab).classList.add('active'); - if (btn.dataset.tab === 'videos') loadVideos(); - }); - }); + async function stopAllJobs() { + if (!confirm('Are you sure you want to stop all active jobs?')) return; + setStatus('loading', 'Stopping all jobs...'); + try { + const response = await fetch('/api/jobs/stop-all', { method: 'POST' }); + if (response.ok) { + setStatus('success', 'All jobs stopped'); + refreshJobs(); + } else { + const text = await response.text(); + setStatus('error', `Error: ${text}`); + } + } catch (err) { + setStatus('error', `Error: ${err}`); + } + } + + async function cleanupJobs() { + setStatus('loading', 'Cleaning up finished jobs...'); + try { + const response = await fetch('/api/cleanup', { method: 'POST' }); + if (response.ok) { + setStatus('success', 'Cleanup completed'); + await refreshJobs(); + } else { + const text = await response.text(); + setStatus('error', `Error: ${text}`); + } + } catch (err) { + setStatus('error', `Error: ${err}`); + } + } + + function setStatus(type, message) { + const el = document.getElementById('statusMessage'); + const icons = { + loading: '', + success: '', + error: '' + }; + el.innerHTML = `${icons[type] || ''} ${message}`; + } + + function startAutoRefresh() { + stopAutoRefresh(); + autoRefreshTimer = setInterval(() => { refreshJobs(); resetCountdown(); }, 60000); + countdownTimer = setInterval(() => { + countdownValue--; + document.getElementById('refreshCountdown').textContent = countdownValue; + if (countdownValue <= 0) countdownValue = 60; + }, 1000); + } + + function stopAutoRefresh() { + if (autoRefreshTimer) clearInterval(autoRefreshTimer); + if (countdownTimer) clearInterval(countdownTimer); + } + + function resetCountdown() { + countdownValue = 60; + document.getElementById('refreshCountdown').textContent = countdownValue; + } // ========== НОВЫЙ КОД: Видео ========== - async function loadVideos() { - try { - const response = await fetch('/api/videos'); - if (!response.ok) throw new Error(await response.text()); - const videos = await response.json(); - renderVideoList(videos); - document.getElementById('videosStatusMessage').innerHTML = `Найдено файлов: ${videos.length}`; - } catch (e) { - document.getElementById('videosStatusMessage').innerHTML = `Ошибка: ${e.message}`; - } - } - - function renderVideoList(videos) { - const container = document.getElementById('videoList'); - if (videos.length === 0) { - container.innerHTML = '

Видеофайлы не найдены.

'; - return; - } - container.innerHTML = videos.map(name => ` -
- ${name} - - -
- `).join(''); - } - function playVideo(filename) { const player = document.getElementById('videoPlayer'); player.src = '/api/videos/' + encodeURIComponent(filename); - document.getElementById('playerModal').style.display = 'flex'; + document.getElementById('playerModal').classList.add('active'); } function closePlayer() { const player = document.getElementById('videoPlayer'); player.pause(); player.src = ''; - document.getElementById('playerModal').style.display = 'none'; + document.getElementById('playerModal').classList.remove('active'); } async function moveVideo(filename) { @@ -345,7 +369,7 @@ const response = await fetch('/api/videos/' + encodeURIComponent(filename) + '/move?remove=true', { method: 'POST' }); if (!response.ok) throw new Error(await response.text()); alert('Файл успешно перемещён.'); - loadVideos(); + refreshJobs(); // обновим список, возможно, файл исчезнет } catch (e) { alert('Ошибка перемещения: ' + e.message); } diff --git a/src/static/style.css b/src/static/style.css index 3b3e2e4..3dae062 100644 --- a/src/static/style.css +++ b/src/static/style.css @@ -552,4 +552,48 @@ tr:hover { .status-message { margin-left: 0; } +} + +/* Модальное окно плеера */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.8); + align-items: center; + justify-content: center; + z-index: 1000; +} + +.modal.active { + display: flex; +} + +.modal-content { + background: var(--bg-secondary); + border-radius: 12px; + padding: 20px; + max-width: 90vw; + box-shadow: var(--shadow-lg); + position: relative; +} + +.modal .close { + position: absolute; + top: 8px; + right: 16px; + font-size: 28px; + color: var(--text-primary); + cursor: pointer; + line-height: 1; +} + +/* Кнопки в таблице (уменьшенный размер) */ +.btn-sm { + padding: 4px 8px; + font-size: 12px; + gap: 4px; } \ No newline at end of file diff --git a/src/web.rs b/src/web.rs index 2a46a1e..30f5a18 100644 --- a/src/web.rs +++ b/src/web.rs @@ -144,10 +144,12 @@ async fn create_file_access( let url = access_config.smb_url.as_ref().unwrap().clone(); let user = access_config.smb_user.clone(); let password = access_config.smb_pass.clone(); + let domain = access_config.smb_domain.clone(); // теперь читаем домен let smb_config = SmbAccessConfig { url, user, password, + domain, }; Ok(Arc::new(SmbFileAccess::new(smb_config))) } @@ -305,6 +307,7 @@ async fn stream_video( State(state): State, headers: axum::http::HeaderMap, ) -> Result { + log::info!("Stream video requested: {}", filename); let file_data = state .source_access .read_file(&filename)