feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization
This commit is contained in:
parent
a88aafa1e5
commit
0e568f1f0a
975 changed files with 130593 additions and 16783 deletions
|
|
@ -0,0 +1,175 @@
|
|||
using System.Net;
|
||||
using Paca.Core.Config;
|
||||
using Paca.Core.Hls;
|
||||
using Paca.Core.Platform;
|
||||
using Xunit;
|
||||
|
||||
namespace Paca.Tests.Core;
|
||||
|
||||
public class DownloadFolderCleanlinessAndPacaIsolationGateTests
|
||||
{
|
||||
[Fact]
|
||||
public void Gate_DownloadMetaPathResolver_CreatesHiddenPacaFolder_AndSubdirectories()
|
||||
{
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), "paca_clean_test_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
try
|
||||
{
|
||||
var outputBase = Path.Combine(tempRoot, "test_video");
|
||||
var partsDir = DownloadMetaPathResolver.GetPartsDirectory(outputBase);
|
||||
var tempPartFile = DownloadMetaPathResolver.GetTempPartFilePath(outputBase);
|
||||
var pacaDir = DownloadMetaPathResolver.GetPacaDirectory(tempRoot);
|
||||
|
||||
Assert.True(Directory.Exists(pacaDir), ".paca 폴더가 생성되어야 한다.");
|
||||
var di = new DirectoryInfo(pacaDir);
|
||||
Assert.True((di.Attributes & FileAttributes.Hidden) != 0, ".paca 폴더는 Hidden 속성을 가져야 한다.");
|
||||
|
||||
// partsDir은 .paca/parts 하위에 존재해야 함
|
||||
Assert.Contains(Path.Combine(".paca", "parts"), partsDir);
|
||||
Assert.DoesNotContain(tempRoot + Path.DirectorySeparatorChar + "test_video.parts", partsDir);
|
||||
|
||||
// tempPartFile은 .paca/temp 하위에 존재해야 함
|
||||
Assert.Contains(Path.Combine(".paca", "temp"), tempPartFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempRoot))
|
||||
{
|
||||
try { Directory.Delete(tempRoot, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gate_DownloadFolder_HlsDownloader_StoresPartsInsidePacaHiddenFolder()
|
||||
{
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), "paca_hls_clean_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
|
||||
// Fake HTTP Listener for sample m3u8
|
||||
var listener = new HttpListener();
|
||||
var port = 58921;
|
||||
listener.Prefixes.Add($"http://127.0.0.1:{port}/");
|
||||
listener.Start();
|
||||
|
||||
var serverTask = Task.Run(async () =>
|
||||
{
|
||||
while (listener.IsListening)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ctx = await listener.GetContextAsync();
|
||||
var path = ctx.Request.Url?.AbsolutePath ?? "";
|
||||
if (path.EndsWith("playlist.m3u8"))
|
||||
{
|
||||
var m3u8 = "#EXTM3U\n#EXT-X-TARGETDURATION:2\n#EXTINF:2.0,\nseg0.ts\n#EXT-X-ENDLIST\n";
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(m3u8);
|
||||
ctx.Response.ContentType = "application/vnd.apple.mpegurl";
|
||||
ctx.Response.OutputStream.Write(bytes);
|
||||
ctx.Response.Close();
|
||||
}
|
||||
else if (path.EndsWith("seg0.ts"))
|
||||
{
|
||||
var seg = new byte[1024];
|
||||
new Random(42).NextBytes(seg);
|
||||
ctx.Response.ContentType = "video/MP2T";
|
||||
ctx.Response.OutputStream.Write(seg);
|
||||
ctx.Response.Close();
|
||||
}
|
||||
}
|
||||
catch { break; }
|
||||
}
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
var config = new AppConfig { Concurrency = 1, OutputFolder = tempRoot };
|
||||
var fakeMuxer = new FakeMediaMuxer();
|
||||
var hls = new HlsDownloader(http, config, fakeMuxer);
|
||||
|
||||
var outputBase = Path.Combine(tempRoot, "my_video");
|
||||
var res = await hls.DownloadAsync(new Uri($"http://127.0.0.1:{port}/playlist.m3u8"), outputBase);
|
||||
|
||||
// 1. 루트 폴더에 .parts 폴더나 .json 파일이 절대 없어야 한다!
|
||||
var rootDirs = Directory.GetDirectories(tempRoot).Select(Path.GetFileName).ToList();
|
||||
Assert.DoesNotContain("my_video.parts", rootDirs);
|
||||
|
||||
var rootFiles = Directory.GetFiles(tempRoot).Select(Path.GetFileName).ToList();
|
||||
Assert.DoesNotContain(".chunkcache.json", rootFiles);
|
||||
Assert.DoesNotContain("chunkcache.json", rootFiles);
|
||||
|
||||
// 2. 최종 mp4 파일은 루트에 존재해야 한다
|
||||
Assert.True(File.Exists(outputBase + ".mp4"), "최종 mp4는 루트에 있어야 함");
|
||||
}
|
||||
finally
|
||||
{
|
||||
listener.Stop();
|
||||
try { await serverTask; } catch { }
|
||||
if (Directory.Exists(tempRoot))
|
||||
{
|
||||
try { Directory.Delete(tempRoot, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Gate_DownloadFolder_DirectDownloader_IsolatesPartFileInPacaHiddenDir()
|
||||
{
|
||||
var tempRoot = Path.Combine(Path.GetTempPath(), "paca_direct_clean_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(tempRoot);
|
||||
|
||||
try
|
||||
{
|
||||
var fakeHandler = new SimpleMockHandler(new byte[512]);
|
||||
using var client = new HttpClient(fakeHandler);
|
||||
var config = new AppConfig { OutputFolder = tempRoot };
|
||||
var downloader = new DirectDownloader(client, config);
|
||||
|
||||
var outputBase = Path.Combine(tempRoot, "direct_file");
|
||||
var final = await downloader.DownloadAsync(new Uri("https://test.paca/video.mp4"), outputBase);
|
||||
|
||||
Assert.True(File.Exists(final));
|
||||
// 루트 폴더에 .part 파일이 없어야 함
|
||||
Assert.False(File.Exists(outputBase + ".part"), "루트 폴더에 .part 파일이 존재해서는 안 됨");
|
||||
|
||||
var rootFiles = Directory.GetFiles(tempRoot).Select(Path.GetFileName).ToList();
|
||||
Assert.DoesNotContain("direct_file.part", rootFiles);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (Directory.Exists(tempRoot))
|
||||
{
|
||||
try { Directory.Delete(tempRoot, recursive: true); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SimpleMockHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly byte[] _data;
|
||||
public SimpleMockHandler(byte[] data) => _data = data;
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
var resp = new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(_data)
|
||||
};
|
||||
resp.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("video/mp4");
|
||||
return Task.FromResult(resp);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FakeMediaMuxer : IMediaMuxer
|
||||
{
|
||||
public Task RemuxAsync(string ffmpeg, string input, string output, CancellationToken ct)
|
||||
{
|
||||
File.Copy(input, output, overwrite: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DownloadDashAsync(string ffmpeg, Uri mpdUrl, string output, double durationSec, HttpClient http, IProgress<DownloadProgress>? progress, CancellationToken ct) => Task.CompletedTask;
|
||||
|
||||
public string? ResolveFfmpeg(AppConfig config) => "fake-ffmpeg";
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue