refactor(1): move Detection (MediaDetector/SocialSiteDetector/DetectionContext) App->Core

This commit is contained in:
Yun Chan 2026-08-15 14:49:02 +09:00
parent b6b7fc341b
commit 1aab8729fa
6 changed files with 6 additions and 6 deletions

View file

@ -1,81 +0,0 @@
using VideoDownloader.Core.Config;
namespace VideoDownloader.App.Detection;
/// <summary>요청(URL + content-type + 길이)과 설정으로 미디어 종류를 판정.</summary>
public static class MediaDetector
{
/// <returns>HLS 또는 Direct, 감지 대상이 아니면 null</returns>
public static MediaKind? Classify(string? uri, string? contentType, long len, AppConfig config)
{
var l = (uri ?? "").ToLowerInvariant();
var c = (contentType ?? "").ToLowerInvariant();
bool isHls = c.Contains("mpegurl") || l.Contains(".m3u8") || l.Contains(".m3u");
if (isHls)
return Enabled(config, "hls") ? MediaKind.Hls : null;
bool isDash = c.Contains("dash+xml") || l.Contains(".mpd");
if (isDash)
return Enabled(config, "dash") ? MediaKind.Dash : null;
long min = config.MinSizeMB * 1024L * 1024L;
if (len >= 0 && len <= min) return null; // 너무 작음
var fmt = MatchFormat(l, c);
if (fmt == null) return null;
return Enabled(config, fmt) ? MediaKind.Direct : null;
}
private static bool Enabled(AppConfig config, string key) => config.EnabledFormats.Contains(key);
/// <summary>형식 키 반환: mp4/webm/mkv/m4v/mov/avi/audio (미매칭 null)</summary>
public static string? MatchFormat(string l, string c)
{
if (l.EndsWith(".mp4") || c.Contains("mp4")) return "mp4";
if (l.EndsWith(".webm") || c.Contains("webm")) return "webm";
if (l.EndsWith(".mkv") || c.Contains("matroska")) return "mkv";
if (l.EndsWith(".m4v")) return "m4v";
if (l.EndsWith(".mov") || c.Contains("quicktime")) return "mov";
if (l.EndsWith(".avi") || c.Contains("x-msvideo")) return "avi";
bool audio = c.StartsWith("audio/") || l.EndsWith(".m4a") || l.EndsWith(".mp3")
|| l.EndsWith(".ogg") || l.EndsWith(".opus") || l.EndsWith(".flac")
|| l.EndsWith(".wav") || l.EndsWith(".aac");
if (audio) return "audio";
if (c.StartsWith("video/")) return "mp4"; // 기타 video 는 mp4 취급
return null;
}
/// <summary>감지 후보(사람에게 보여줄 만한) 판정 - DASH/MSE/세그먼트 등</summary>
public static bool LooksLikeCandidate(string l, string c) =>
l.Contains(".m4s") || l.Contains("segment") || l.Contains("/init") || l.Contains(".ts?")
|| l.Contains(".mpd") || c.Contains("dash+xml") || c.Contains("mp2t") || c.Contains("octet-stream");
/// <summary>#15 본문 선두 magic byte 로 실제 포맷 판별 (위장 image/* 등 검증용).
/// 이미지 포맷(GIF/PNG/JPEG/WebP)은 null 반환 — 위장 트래킹 픽셀이 미디어로 오탐되는 것 방지.
/// (GIF 'G'=0x47 가 MPEG-TS sync byte 와 겹쳐 ts 로 오탐되던 문제 수정)</summary>
public static string? ProbeFormat(ReadOnlySpan<byte> h)
{
if (h.Length < 4) return null;
// 이미지 포맷 → 미디어 아님
if (h.Length >= 3 && h[0] == 'G' && h[1] == 'I' && h[2] == 'F') return null; // GIF
if (h.Length >= 8 && h[0] == 0x89 && h[1] == 0x50 && h[2] == 0x4E && h[3] == 0x47) return null; // PNG
if (h.Length >= 3 && h[0] == 0xFF && h[1] == 0xD8 && h[2] == 0xFF) return null; // JPEG
if (h.Length >= 12 && h[0] == 0x52 && h[1] == 0x49 && h[2] == 0x46 && h[3] == 0x46) // RIFF
{
if (h[8] == 'W' && h[9] == 'E' && h[10] == 'B' && h[11] == 'P') return null; // WebP(이미지)
if (h[8] == 'W' && h[9] == 'A' && h[10] == 'V' && h[11] == 'E') return "wav"; // WAV(오디오)
return null;
}
if (h.Length >= 12 && h[4] == 'f' && h[5] == 't' && h[6] == 'y' && h[7] == 'p') return "mp4"; // ftyp box
if (h[0] == 0x47) return "ts"; // MPEG-TS sync (GIF 는 위에서 걸러짐)
if (h[0] == 0x1A && h[1] == 0x45 && h[2] == 0xDF && h[3] == 0xA3) return "webm"; // EBML
if (h[0] == 'I' && h[1] == 'D' && h[2] == '3') return "mp3"; // ID3
if (h[0] == 0xFF && (h[1] & 0xE0) == 0xE0) return "mp3"; // MP3 frame
if (h[0] == 0x66 && h[1] == 0x4C && h[2] == 0x61 && h[3] == 0x43) return "flac"; // fLaC
if (h[0] == 0x4F && h[1] == 0x67 && h[2] == 0x67 && h[3] == 0x53) return "ogg"; // OggS
return null;
}
}