114 lines
5.6 KiB
C#
114 lines
5.6 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Windows.Input;
|
|
using Avalonia.Threading;
|
|
using VideoDownloader.Core.Config;
|
|
using VideoDownloader.Core.Dash;
|
|
using VideoDownloader.Core.Hls;
|
|
using VideoDownloader.Core.Models;
|
|
using VideoDownloader.Core.Social;
|
|
using VideoDownloader.Core.Util;
|
|
|
|
namespace VideoDownloader.Avalonia;
|
|
|
|
/// <summary>Phase 2 Gate #0: 크로스플랫폼 셸의 최소 뷰모델.
|
|
/// URL 을 붙여넣으면 유형(HLS/DASH/소셜/직링크) 을 판별해 Core 다운로더를 직접 호출해 진행률을 표시.
|
|
/// 브라우저/감지/쿠키(Gate #1) 는 아직 없으므로 DownloadManager 가 아닌 Core 엔진을 직접 사용.</summary>
|
|
public class MainViewModel : INotifyPropertyChanged
|
|
{
|
|
private readonly AppConfig _config = new();
|
|
private CancellationTokenSource? _cts;
|
|
|
|
private string _url = "";
|
|
private string _status = "URL 을 붙여넣고 받기를 누르세요";
|
|
private double _progress;
|
|
private bool _isBusy;
|
|
private string _result = "";
|
|
|
|
public string Url { get => _url; set { _url = value; On(); _downloadCommand.RaiseCanExecuteChanged(); } }
|
|
public string Status { get => _status; set { _status = value; On(); } }
|
|
public double Progress { get => _progress; set { _progress = value; On(); } }
|
|
public bool IsBusy { get => _isBusy; set { _isBusy = value; On(); _downloadCommand.RaiseCanExecuteChanged(); _cancelCommand.RaiseCanExecuteChanged(); } }
|
|
public string Result { get => _result; set { _result = value; On(); } }
|
|
public ObservableCollection<string> Log { get; } = new();
|
|
|
|
private readonly RelayCommand _downloadCommand;
|
|
private readonly RelayCommand _cancelCommand;
|
|
public ICommand DownloadCommand => _downloadCommand;
|
|
public ICommand CancelCommand => _cancelCommand;
|
|
|
|
public MainViewModel()
|
|
{
|
|
_downloadCommand = new RelayCommand(_ => _ = DownloadAsync(), _ => !IsBusy && !string.IsNullOrWhiteSpace(Url));
|
|
_cancelCommand = new RelayCommand(_ => Cancel(), _ => IsBusy);
|
|
}
|
|
|
|
public void Cancel() { try { _cts?.Cancel(); } catch { } Status = "취소 중..."; }
|
|
|
|
private async Task DownloadAsync()
|
|
{
|
|
if (!Uri.TryCreate(Url.Trim(), UriKind.Absolute, out var uri))
|
|
{ Status = "올바른 URL 이 아닙니다"; return; }
|
|
|
|
// UI 스레드에서 미리 유형 판별 + 상태 표시(백그라운드에서 INPC 속성을 건드리지 않도록).
|
|
var kind = Classify(uri);
|
|
IsBusy = true; Result = ""; Progress = 0; Log.Clear();
|
|
Status = kind switch { Kind.Social => "yt-dlp 소셜 다운로드", Kind.Hls => "HLS 다운로드", Kind.Dash => "DASH 다운로드", _ => "직접 다운로드" };
|
|
|
|
_cts = new CancellationTokenSource();
|
|
var ct = _cts.Token;
|
|
var progress = new Progress<DownloadProgress>(p => Dispatcher.UIThread.Post(() =>
|
|
{
|
|
if (p.OverridePercent >= 0) Progress = p.OverridePercent;
|
|
Status = !string.IsNullOrEmpty(p.Detail) ? p.Detail
|
|
: (p.Stage == "downloading" && p.Total > 0 ? $"{p.Completed}/{p.Total}" : p.Stage);
|
|
if (p.OverridePercent < 0 && !string.IsNullOrEmpty(p.Detail)) Log.Insert(0, p.Detail);
|
|
}));
|
|
|
|
try
|
|
{
|
|
var name = FileNameUtil.Sanitize(FileNameUtil.DeriveName(uri));
|
|
var outputBase = Path.Combine(_config.OutputFolder, name);
|
|
Directory.CreateDirectory(_config.OutputFolder);
|
|
string final = await Task.Run(() => DownloadByKindAsync(uri, kind, outputBase, progress, ct), ct);
|
|
|
|
Progress = 100;
|
|
Result = $"완료: {final}";
|
|
Status = "완료";
|
|
}
|
|
catch (OperationCanceledException) { Status = "취소됨"; }
|
|
catch (Exception e) { Status = "오류"; Result = $"{e.GetType().Name}: {e.Message}"; }
|
|
finally { IsBusy = false; }
|
|
}
|
|
|
|
// 백그라운드 스레드에서 실행. UI 속성 미참조(진행률은 progress 콜백→UI 디스패처로 전달).
|
|
private async Task<string> DownloadByKindAsync(Uri uri, Kind kind, string outputBase, IProgress<DownloadProgress> progress, CancellationToken ct)
|
|
{
|
|
using var http = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
|
http.DefaultRequestHeaders.UserAgent.ParseAdd(_config.UserAgent);
|
|
return kind switch
|
|
{
|
|
Kind.Social => await new YtDlpDownloader(_config)
|
|
.DownloadAsync(uri, outputBase, new FormatOption("최고 화질 (MP4)", "bv*+ba/b"), null, progress, ct),
|
|
Kind.Hls => await new HlsDownloader(http, _config).DownloadAsync(uri, outputBase, progress, ct),
|
|
Kind.Dash => await new DashDownloader(http, _config).DownloadAsync(uri, outputBase, progress, ct),
|
|
_ => await new DirectDownloader(http, _config).DownloadAsync(uri, outputBase, progress, ct),
|
|
};
|
|
}
|
|
|
|
private enum Kind { Social, Hls, Dash, Direct }
|
|
private static Kind Classify(Uri uri)
|
|
{
|
|
string host = uri.Host.ToLowerInvariant(), path = uri.AbsolutePath.ToLowerInvariant();
|
|
bool social = host.Contains("youtube") || host.Contains("youtu.be") || host.Contains("instagram")
|
|
|| host.Contains("tiktok") || host.Contains("x.com") || host.Contains("twitter") || host.Contains("facebook");
|
|
if (social) return Kind.Social;
|
|
if (path.EndsWith(".m3u8")) return Kind.Hls;
|
|
if (path.EndsWith(".mpd")) return Kind.Dash;
|
|
return Kind.Direct;
|
|
}
|
|
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
private void On([CallerMemberName] string? p = null) => PropertyChanged?.Invoke(this, new(p));
|
|
}
|