Phase D (T2-10): media playlist - auto-collect detected media, flyout management, player window (native+hls.js+dash.js bundles), persistence, auto-next (101 tests GREEN)
This commit is contained in:
parent
406957088f
commit
40c1b39b70
8 changed files with 491 additions and 0 deletions
118
VideoDownloader.App/Browser/Features/MediaPlaylist.cs
Normal file
118
VideoDownloader.App/Browser/Features/MediaPlaylist.cs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace VideoDownloader.App.Browser.Features;
|
||||
|
||||
/// <summary>재생목록 트랙. Kind: Direct/Hls/Dash/Social.</summary>
|
||||
public sealed record MediaTrack(
|
||||
string Url,
|
||||
string Title,
|
||||
string Kind,
|
||||
DateTime AddedAt,
|
||||
bool Played = false,
|
||||
string? Referer = null,
|
||||
string? LocalFile = null);
|
||||
|
||||
/// <summary>
|
||||
/// T2-10 미디어 재생목록(Brave Playlist 패턴): 브라우저에서 감지된 미디어를
|
||||
/// 쌓아두고 앱 내에서 연속 재생. JSON 영속(playlist.json).
|
||||
/// </summary>
|
||||
public sealed class MediaPlaylist
|
||||
{
|
||||
public ObservableCollection<MediaTrack> Tracks { get; } = new();
|
||||
|
||||
public int CurrentIndex { get; set; } = -1;
|
||||
|
||||
[JsonIgnore]
|
||||
public MediaTrack? Current => CurrentIndex >= 0 && CurrentIndex < Tracks.Count ? Tracks[CurrentIndex] : null;
|
||||
|
||||
public event Action? Changed;
|
||||
|
||||
/// <summary>추가(중복 URL 무시). 새 항목이 첫 트랙이면 자동 선택.</summary>
|
||||
public void Add(MediaTrack track)
|
||||
{
|
||||
if (Tracks.Any(t => t.Url == track.Url)) return;
|
||||
Tracks.Add(track);
|
||||
if (CurrentIndex < 0) CurrentIndex = Tracks.Count - 1;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Remove(MediaTrack track)
|
||||
{
|
||||
var idx = Tracks.IndexOf(track);
|
||||
if (idx < 0) return;
|
||||
Tracks.RemoveAt(idx);
|
||||
if (CurrentIndex >= Tracks.Count) CurrentIndex = Tracks.Count - 1;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Tracks.Clear();
|
||||
CurrentIndex = -1;
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>다음 트랙 인덱스(순환). 반환 후 CurrentIndex 갱신.</summary>
|
||||
public int Next()
|
||||
{
|
||||
if (Tracks.Count == 0) return -1;
|
||||
CurrentIndex = (CurrentIndex + 1) % Tracks.Count;
|
||||
return CurrentIndex;
|
||||
}
|
||||
|
||||
/// <summary>이전 트랙 인덱스(순환).</summary>
|
||||
public int Previous()
|
||||
{
|
||||
if (Tracks.Count == 0) return -1;
|
||||
CurrentIndex = (CurrentIndex - 1 + Tracks.Count) % Tracks.Count;
|
||||
return CurrentIndex;
|
||||
}
|
||||
|
||||
public void MarkPlayed(MediaTrack track)
|
||||
{
|
||||
var idx = Tracks.IndexOf(track);
|
||||
if (idx < 0) return;
|
||||
Tracks[idx] = track with { Played = true };
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public void Save(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
var dto = new { tracks = Tracks.ToList(), currentIndex = CurrentIndex };
|
||||
var tmp = path + ".tmp";
|
||||
File.WriteAllText(tmp, JsonSerializer.Serialize(dto));
|
||||
File.Move(tmp, path, overwrite: true);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public void Load(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path)) return;
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(path));
|
||||
var root = doc.RootElement;
|
||||
Tracks.Clear();
|
||||
if (root.TryGetProperty("tracks", out var arr))
|
||||
foreach (var t in arr.EnumerateArray())
|
||||
{
|
||||
var track = t.Deserialize<MediaTrack>();
|
||||
if (track != null) Tracks.Add(track);
|
||||
}
|
||||
CurrentIndex = root.TryGetProperty("currentIndex", out var ci) ? ci.GetInt32() : -1;
|
||||
if (CurrentIndex >= Tracks.Count) CurrentIndex = Tracks.Count - 1;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Tracks.Clear();
|
||||
CurrentIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue