M0: remote access server - Kestrel embedded (token auth/pair/queue API/library/range streaming/web companion zero-install), QR pairing window, DownloadQueueAdapter, auto-start+palette commands. Server E2E green (102 tests)

This commit is contained in:
Yun Chan 2026-08-15 15:30:35 +09:00
parent 40c1b39b70
commit bb569acd96
15 changed files with 734 additions and 0 deletions

View file

@ -40,6 +40,9 @@ public partial class MainWindow : Window
private readonly ObservableCollection<HistoryEntry> _history = new();
private bool _suggesting; // 주소창 인라인 자동완성 도중 (TextChanged 재귀 방지)
// M0: 원격 접근 서버(모바일 연동)
private Server.ServerHost? _remoteServer;
// Phase D: 미디어 재생목록 (T2-10)
private readonly MediaPlaylist _playlist = new();
private PlayerWindow? _player;
@ -243,6 +246,23 @@ public partial class MainWindow : Window
};
_speedTimer.Start();
// M0: 설정돼 있으면 원격 접근 서버 자동 시작
if (_config.RemoteAccessEnabled && !string.IsNullOrEmpty(_config.RemoteAccessToken))
{
try
{
_remoteServer = await Server.ServerHost.StartAsync(new Server.ServerOptions
{
Port = 0,
Token = _config.RemoteAccessToken!,
LibraryFolder = Path.GetFullPath(_config.OutputFolder),
Queue = new Services.DownloadQueueAdapter(_manager, Dispatcher),
});
Log($"원격 접근 서버 포트 {_remoteServer.Port}");
}
catch { }
}
// T2-2 탭 절전: 1분마다 유휴(10분+) 비활성 탭 서스펜드
_suspendService = new TabSuspendService(TimeSpan.FromMinutes(10), tab =>
{
@ -879,6 +899,7 @@ public partial class MainWindow : Window
// T1-10 세션 저장: 탭 URL/활성/핀 보존
try { if (_tabs != null) TabSessionStore.Save(SessionPath, _tabs.CaptureSession()); } catch { }
try { _playlist.Save(Path.Combine(AppDataDir, "playlist.json")); } catch { } // T2-10
try { if (_remoteServer != null) _ = _remoteServer.DisposeAsync(); } catch { } // M0
}
// ---- 개인정보 보호 모드 + 브라우저 데이터 관리 ----
@ -1152,6 +1173,8 @@ public partial class MainWindow : Window
_palette.Register("이 페이지 즐겨찾기", "bookmark add star", () => AddBookmark_Click(this, new RoutedEventArgs()));
_palette.Register("미디어 재생목록", "playlist play media 음악 영상", () => { PlaylistPopup.IsOpen = true; });
_palette.Register("재생목록 재생", "playlist play start", OpenPlayer);
_palette.Register("기기 연결 (QR 페어링)", "remote pair qr mobile phone", ShowPairing);
_palette.Register("원격 접근 켜기/끄기", "remote server toggle lan", async () => await ToggleRemoteAccess());
_palette.RegisterSeparator();
_palette.RegisterDynamic(() => _tabs.Tabs
.Select(t => new PaletteCommand($"탭 전환: {Trunc(t.Title, 40)}", "tab go switch", () => _tabs.Activate(t)))
@ -1486,6 +1509,56 @@ public partial class MainWindow : Window
Log($"워크스페이스 '{ws.Name}' 전환 ({ws.Urls.Count} 탭)");
}
// ---- M0 원격 접근(모바일 연동) ----
private async Task ToggleRemoteAccess()
{
if (_remoteServer != null)
{
await _remoteServer.DisposeAsync();
_remoteServer = null;
_config.RemoteAccessEnabled = false;
ConfigStore.Save(_config);
Log("원격 접근을 껐습니다");
return;
}
var token = _config.RemoteAccessToken;
if (string.IsNullOrEmpty(token))
{
token = Convert.ToBase64String(System.Security.Cryptography.RandomNumberGenerator.GetBytes(24))
.TrimEnd('=').Replace('+', '-').Replace('/', '_');
_config.RemoteAccessToken = token;
}
_config.RemoteAccessEnabled = true;
ConfigStore.Save(_config);
try
{
_remoteServer = await Server.ServerHost.StartAsync(new Server.ServerOptions
{
Port = 0,
Token = token,
LibraryFolder = Path.GetFullPath(_config.OutputFolder),
Queue = new Services.DownloadQueueAdapter(_manager, Dispatcher),
});
Log($"원격 접근 켜짐 — 포트 {_remoteServer.Port}");
var pair = new Browser.PairingWindow(_remoteServer, Environment.MachineName) { Owner = this };
pair.Show();
}
catch (Exception ex)
{
_remoteServer = null;
Log($"원격 접근 시작 실패(방화벽 확인): {ex.Message}");
}
}
private async void ShowPairing()
{
if (_remoteServer == null) { await ToggleRemoteAccess(); return; }
var pair = new Browser.PairingWindow(_remoteServer, Environment.MachineName) { Owner = this };
pair.Show();
}
// ---- T2-10 미디어 재생목록 ----
private void AddToPlaylist(string url, string title, string kind, string? referer)
{