From e30736c3e7e76b33d14931948effff8d01be48de Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Fri, 3 Apr 2026 22:23:47 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E7=99=BB=E5=BD=95=20?= =?UTF-8?q?=E8=BF=9B=E4=B8=80=E6=AD=A5=E5=AE=8C=E5=96=84=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/js/main.js | 120 ++++++++++++++++++++++++++++++++++++++++++---- server.js | 16 +++++-- 2 files changed, 123 insertions(+), 13 deletions(-) diff --git a/public/js/main.js b/public/js/main.js index a3734dc..efe1920 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -72,6 +72,8 @@ document.addEventListener('DOMContentLoaded', () => { let s3Username = ''; let s3Password = ''; let s3AuthHeaders = {}; + const SAVED_AUTH_STORAGE_KEY = 'media-coding-web:s3-auth'; + const SAVED_BUCKET_STORAGE_KEY = 'media-coding-web:selected-bucket'; // Seek state let videoDuration = 0; @@ -768,6 +770,62 @@ document.addEventListener('DOMContentLoaded', () => { if (s3Password) s3AuthHeaders['X-S3-Password'] = s3Password; }; + const saveAuthState = (username, password) => { + try { + window.localStorage.setItem(SAVED_AUTH_STORAGE_KEY, JSON.stringify({ + username, + password + })); + } catch (error) { + console.warn('Failed to persist auth state:', error); + } + }; + + const loadAuthState = () => { + try { + const rawValue = window.localStorage.getItem(SAVED_AUTH_STORAGE_KEY); + if (!rawValue) return null; + const parsed = JSON.parse(rawValue); + if (!parsed || typeof parsed.username !== 'string' || typeof parsed.password !== 'string') { + return null; + } + return parsed; + } catch (error) { + console.warn('Failed to read auth state:', error); + return null; + } + }; + + const clearAuthState = () => { + try { + window.localStorage.removeItem(SAVED_AUTH_STORAGE_KEY); + window.localStorage.removeItem(SAVED_BUCKET_STORAGE_KEY); + } catch (error) { + console.warn('Failed to clear auth state:', error); + } + }; + + const saveSelectedBucket = (bucketName) => { + try { + if (bucketName) { + window.localStorage.setItem(SAVED_BUCKET_STORAGE_KEY, bucketName); + } else { + window.localStorage.removeItem(SAVED_BUCKET_STORAGE_KEY); + } + } catch (error) { + console.warn('Failed to persist selected bucket:', error); + } + }; + + const loadSelectedBucket = () => { + try { + return window.localStorage.getItem(SAVED_BUCKET_STORAGE_KEY) || ''; + } catch (error) { + console.warn('Failed to read selected bucket:', error); + return ''; + } + }; + const showLogin = () => { if (loginScreen) loginScreen.classList.remove('hidden'); if (appContainer) appContainer.classList.add('hidden'); @@ -801,16 +859,19 @@ document.addEventListener('DOMContentLoaded', () => { loginError.classList.add('hidden'); }; - const login = async () => { - if (!loginUsernameInput || !loginPasswordInput) return; - const username = loginUsernameInput.value.trim(); - const password = loginPasswordInput.value; + const login = async (options = {}) => { + if (!loginUsernameInput || !loginPasswordInput) return false; + const username = typeof options.username === 'string' ? options.username.trim() : loginUsernameInput.value.trim(); + const password = typeof options.password === 'string' ? options.password : loginPasswordInput.value; + const skipErrorUi = options.skipErrorUi === true; if (!username || !password) { - showLoginError('Please enter both access key and secret key.'); - return; + if (!skipErrorUi) { + showLoginError('Please enter both access key and secret key.'); + } + return false; } loginBtn.disabled = true; - loginBtn.textContent = 'Logging in...'; + loginBtn.textContent = options.isRestoring ? 'Restoring...' : 'Logging in...'; clearLoginError(); try { @@ -824,17 +885,31 @@ document.addEventListener('DOMContentLoaded', () => { if (!Array.isArray(data.buckets) || data.buckets.length === 0) { throw new Error('No buckets available for this account'); } + saveAuthState(username, password); + if (loginUsernameInput) loginUsernameInput.value = username; + if (loginPasswordInput) loginPasswordInput.value = password; renderBuckets(data.buckets); showApp(); - selectedBucket = data.buckets[0].Name; + const savedBucket = loadSelectedBucket(); + selectedBucket = data.buckets.some((bucket) => bucket.Name === savedBucket) + ? savedBucket + : data.buckets[0].Name; if (bucketSelect) bucketSelect.value = selectedBucket; + saveSelectedBucket(selectedBucket); loadConfig(); - connectWebSocket(); + if (!ws) { + connectWebSocket(); + } await fetchVideos(selectedBucket); + return true; } catch (err) { console.error('Login error:', err); - showLoginError(err.message); + clearAuthState(); + if (!skipErrorUi) { + showLoginError(err.message); + } setAuthHeaders('', ''); + return false; } finally { if (loginBtn) { loginBtn.disabled = false; @@ -843,6 +918,22 @@ document.addEventListener('DOMContentLoaded', () => { } }; + const restoreSavedSession = async () => { + const savedAuth = loadAuthState(); + if (!savedAuth) { + return false; + } + + if (loginUsernameInput) loginUsernameInput.value = savedAuth.username; + if (loginPasswordInput) loginPasswordInput.value = savedAuth.password; + return login({ + username: savedAuth.username, + password: savedAuth.password, + isRestoring: true, + skipErrorUi: true + }); + }; + const clearDownloadCache = async () => { if (!clearDownloadCacheBtn) return; clearDownloadCacheBtn.disabled = true; @@ -1282,10 +1373,19 @@ document.addEventListener('DOMContentLoaded', () => { if (bucketSelect) { bucketSelect.addEventListener('change', async (event) => { selectedBucket = event.target.value; + saveSelectedBucket(selectedBucket); await fetchVideos(selectedBucket); }); } // Initial state: require login before loading data showLogin(); + restoreSavedSession().then((restored) => { + if (!restored) { + showLogin(); + } + }).catch((error) => { + console.error('Session restore failed:', error); + showLogin(); + }); }); diff --git a/server.js b/server.js index ba273d9..f110723 100644 --- a/server.js +++ b/server.js @@ -451,13 +451,15 @@ const clearConvertCache = () => { app.get('/api/buckets', async (req, res) => { try { const auth = extractS3Credentials(req); + console.log(`[s3] list buckets start endpoint=${defaultS3ClientConfig.endpoint || 'aws-default'} region=${defaultS3ClientConfig.region} authProvided=${Boolean(auth.username)}`); const s3Client = createS3Client(auth); const command = new ListBucketsCommand({}); const response = await s3Client.send(command); const buckets = response.Buckets || []; + console.log(`[s3] list buckets complete count=${buckets.length}`); res.json({ buckets }); } catch (error) { - console.error('Error listing buckets:', error); + console.error('[s3] list buckets failed:', error); res.status(500).json({ error: 'Failed to list buckets', detail: error.message }); } }); @@ -473,14 +475,21 @@ app.get('/api/videos', async (req, res) => { const auth = extractS3Credentials(req); const s3Client = createS3Client(auth); let continuationToken; + let pageNumber = 0; + + console.log(`[s3] scan bucket start bucket=${bucket} endpoint=${defaultS3ClientConfig.endpoint || 'aws-default'} authProvided=${Boolean(auth.username)}`); do { + pageNumber += 1; + console.log(`[s3] scan bucket page request bucket=${bucket} page=${pageNumber} continuationToken=${continuationToken || 'none'}`); const command = new ListObjectsV2Command({ Bucket: bucket, ContinuationToken: continuationToken, }); const response = await s3Client.send(command); - allObjects.push(...(response.Contents || [])); + const pageItems = response.Contents || []; + allObjects.push(...pageItems); + console.log(`[s3] scan bucket page result bucket=${bucket} page=${pageNumber} objects=${pageItems.length} truncated=${Boolean(response.IsTruncated)} nextToken=${response.NextContinuationToken || 'none'} scannedTotal=${allObjects.length}`); continuationToken = response.IsTruncated ? response.NextContinuationToken : undefined; } while (continuationToken); @@ -499,9 +508,10 @@ app.get('/api/videos', async (req, res) => { return videoExtensions.some(ext => lowerKey.endsWith(ext)); }); + console.log(`[s3] scan bucket complete bucket=${bucket} pages=${pageNumber} objects=${allObjects.length} videos=${videos.length}`); res.json({ videos }); } catch (error) { - console.error('Error fetching videos:', error); + console.error(`[s3] scan bucket failed bucket=${req.query.bucket || BUCKET_NAME || 'unknown'}:`, error); res.status(500).json({ error: 'Failed to fetch videos from S3', detail: error.message }); } });