add esbuild - my bad sorry

beta build service for youtube
This commit is contained in:
PatrickSt1991
2026-01-24 18:23:33 +01:00
parent 867c7c2c6e
commit 63f37a5cfa
6 changed files with 244 additions and 7 deletions

1
.gitignore vendored
View File

@@ -178,7 +178,6 @@ DocProject/Help/html
# Click-Once directory
publish/
esbuild/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml

View File

@@ -17,6 +17,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin
private readonly JellyfinIndex _indexHtml;
private readonly JellyfinDiagnostic _diagnostic;
private readonly FixYouTube _youTube;
private readonly YouTubeWebService _youtubeService;
private readonly CustomCss _customCss;
public JellyfinPackagePatcher(HttpClient http)
@@ -27,6 +28,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin
_indexHtml = new JellyfinIndex(http, api, plugins);
_diagnostic = new JellyfinDiagnostic();
_youTube = new FixYouTube();
_youtubeService = new YouTubeWebService();
_customCss = new CustomCss();
}
@@ -42,7 +44,10 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin
if (AppSettings.Default.PatchYoutubePlugin)
{
await _youTube.CorsAsync(ws);
await _youTube.FixAsync(ws);
await _youTube.PatchPluginAsync(ws);
//await _youtubeService.UpdateCorsAsync(ws);
//await _youtubeService.CreatePackageJsonAsync(ws);
//await _youtubeService.CreateYouTubeResolverAsync(ws);
}
// Always update server address

View File

@@ -1,22 +1,23 @@
using Jellyfin2Samsung.Helpers.Core;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
namespace Jellyfin2Samsung.Helpers.Jellyfin.Fixes
{
public class FixYouTube
{
public async Task FixAsync(PackageWorkspace ws)
public async Task PatchPluginAsync(PackageWorkspace ws)
{
var www = Path.Combine(ws.Root, "www");
foreach (var file in Directory.GetFiles(www, "youtubePlayer-plugin.*.js"))
{
var js = await File.ReadAllTextAsync(file);
var apiBase = AppSettings.Default.LocalYoutubeServer.TrimEnd('/');
if (js.Contains("__NATIVE_STABLE_V1__"))
continue;
string nativeCode = $@"
@@ -25,7 +26,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Fixes
if (window.__NATIVE_STABLE_V1__) return;
window.__NATIVE_STABLE_V1__ = true;
var API_BASE = '{apiBase}';
var API_BASE = 'http://127.0.0.1:8123';
function jfLog() {{
try {{
@@ -191,7 +192,7 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Fixes
/* === Fetch local MP4 === */
jfLog('fetching mp4');
fetch(API_BASE + '/file', {{
fetch(API_BASE + '/stream', {{
method: 'POST',
headers: {{ 'Content-Type': 'application/json' }},
body: JSON.stringify({{
@@ -260,4 +261,236 @@ namespace Jellyfin2Samsung.Helpers.Jellyfin.Fixes
doc.Save(path);
}
}
public class YouTubeWebService
{
public async Task UpdateCorsAsync(PackageWorkspace ws)
{
string path = Path.Combine(ws.Root, "config.xml");
XDocument doc;
using (var stream = File.OpenRead(path))
{
doc = XDocument.Load(stream);
}
XNamespace ns = "http://www.w3.org/ns/widgets";
XNamespace tizenNs = "http://tizen.org/ns/widgets";
// Ensure required_version is modern
var appTag = doc.Root.Element(tizenNs + "application");
if (appTag != null)
appTag.SetAttributeValue("required_version", "8.0");
// Remove old service tags to prevent duplicates
doc.Root.Elements(tizenNs + "service").Remove();
// Derive packageId from <tizen:application package="..."> (fallback to known)
var packageId = (string)appTag?.Attribute("package") ?? "AprZAARz4r";
// Tizen docs pattern: service id = packageId + ".Something"
var serviceId = packageId + ".ytresolver";
// ✅ Correct service definition per Tizen Web Service docs:
// type="ui" and content src points to JS file, not folder
var serviceElement = new XElement(tizenNs + "service",
new XAttribute("id", serviceId),
new XAttribute("type", "ui"),
new XElement(tizenNs + "content", new XAttribute("src", "service/service.js")),
new XElement(tizenNs + "name", "ytresolver"),
new XElement(tizenNs + "description", "YouTube Stream Resolver Service")
);
// Capture elements before rebuilding
var allElements = doc.Root.Elements().ToList();
doc.Root.RemoveNodes();
// Pull W3C features
var w3cFeatures = allElements.Where(e => e.Name == ns + "feature").ToList();
// Pull existing tizen:feature elements (keep any that were already there)
var tizenFeatures = allElements.Where(e => e.Name == tizenNs + "feature").ToList();
// ✅ Ensure <tizen:feature name="http://tizen.org/feature/web.service"/> exists
if (!tizenFeatures.Any(e => (string)e.Attribute("name") == "http://tizen.org/feature/web.service"))
{
tizenFeatures.Add(new XElement(tizenNs + "feature",
new XAttribute("name", "http://tizen.org/feature/web.service")));
}
// Rebuild XML in a stable order (your “strict order” approach)
doc.Root.Add(allElements.Where(e => e.Name == ns + "name"));
doc.Root.Add(allElements.Where(e => e.Name == ns + "description"));
doc.Root.Add(allElements.Where(e => e.Name == ns + "author"));
doc.Root.Add(allElements.Where(e => e.Name == ns + "icon"));
doc.Root.Add(allElements.Where(e => e.Name == ns + "content"));
// W3C feature/access/navigation first
doc.Root.Add(w3cFeatures);
doc.Root.Add(new XElement(ns + "access",
new XAttribute("origin", "*"),
new XAttribute("subdomains", "true")));
doc.Root.Add(new XElement(ns + "allow-navigation",
new XAttribute("href", "*")));
// Then Tizen feature + application + service
doc.Root.Add(tizenFeatures);
if (appTag != null) doc.Root.Add(appTag);
doc.Root.Add(serviceElement);
// Then remaining tizen elements
doc.Root.Add(allElements.Where(e => e.Name == tizenNs + "metadata"));
doc.Root.Add(allElements.Where(e => e.Name == tizenNs + "profile"));
doc.Root.Add(allElements.Where(e => e.Name == tizenNs + "setting"));
// Privileges (optional for service; still OK to keep)
string[] privileges = {
"http://tizen.org/privilege/internet",
"http://tizen.org/privilege/filesystem.read",
"http://tizen.org/privilege/filesystem.write"
};
foreach (var priv in privileges)
doc.Root.Add(new XElement(tizenNs + "privilege", new XAttribute("name", priv)));
// Save without BOM
var settings = new XmlWriterSettings
{
Encoding = new UTF8Encoding(false),
Indent = true,
NewLineChars = "\n"
};
using (var writer = XmlWriter.Create(path, settings))
{
doc.Save(writer);
}
}
public async Task CreatePackageJsonAsync(PackageWorkspace ws)
{
string serviceDir = Path.Combine(ws.Root, "service");
string packageJsonPath = Path.Combine(serviceDir, "package.json");
if (!Directory.Exists(serviceDir)) Directory.CreateDirectory(serviceDir);
string packageJsonContent = "{\n \"name\": \"ytresolver\",\n \"version\": \"1.0.0\",\n \"main\": \"service.js\",\n \"dependencies\": {}\n}";
// 6. FIX 3: Save package.json without BOM
var utf8NoBom = new UTF8Encoding(false); // <--- Critical
await File.WriteAllTextAsync(packageJsonPath, packageJsonContent, utf8NoBom);
}
public async Task CreateYouTubeResolverAsync(PackageWorkspace ws)
{
string serviceDir = Path.Combine(ws.Root, "service");
string serviceJsPath = Path.Combine(serviceDir, "service.js");
if (!Directory.Exists(serviceDir))
Directory.CreateDirectory(serviceDir);
// ES2015-friendly (no /s flag). Uses module.exports.onStart/onStop.
string serviceJsContent = @"const http = require('http');
const https = require('https');
const PORT = 8123;
let server = null;
function fetchUrl(url, headers) {
headers = headers || {};
return new Promise(function(resolve, reject) {
https.get(url, { headers: headers }, function(res) {
let data = '';
res.on('data', function(c) { data += c; });
res.on('end', function() { resolve(data); });
}).on('error', reject);
});
}
function extractPlayerResponse(html) {
// NOTE: No /s flag (dotAll) to keep compatibility
const m = html.match(/ytInitialPlayerResponse\\s*=\\s*(\\{[\\s\\S]*?\\});/);
if (!m) return null;
try {
return JSON.parse(m[1]);
} catch (e) {
return null;
}
}
function resolveStream(youtubeUrl) {
return fetchUrl(youtubeUrl, {
'User-Agent': 'Mozilla/5.0 (SMART-TV; Tizen)'
}).then(function(html) {
const pr = extractPlayerResponse(html);
if (!pr || !pr.streamingData || !pr.streamingData.formats) return null;
const formats = pr.streamingData.formats;
for (let i = 0; i < formats.length; i++) {
const mt = formats[i].mimeType || '';
if (mt.indexOf('video/mp4') !== -1 && mt.indexOf('avc1') !== -1) {
return formats[i].url || null;
}
}
return null;
});
}
function requestHandler(req, res) {
if (req.method === 'POST' && req.url === '/stream') {
let body = '';
req.on('data', function(c) { body += c; });
req.on('end', function() {
try {
const data = JSON.parse(body || '{}');
const url = data.url;
if (!url) throw new Error('missing url');
resolveStream(url).then(function(streamUrl) {
if (!streamUrl) throw new Error('no stream');
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ mode: 'stream', url: streamUrl }));
}).catch(function(e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: e.message }));
});
} catch (e) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: e.message }));
}
});
return;
}
res.writeHead(404);
res.end();
}
// Tizen Web Service lifecycle (recommended)
module.exports.onStart = function() {
if (server) return;
server = http.createServer(requestHandler);
server.listen(PORT, '127.0.0.1', function() {
console.log('[ytresolver] listening on', PORT);
});
};
module.exports.onStop = function() {
if (!server) return;
try {
server.close(function() {
console.log('[ytresolver] stopped');
});
} finally {
server = null;
}
};
";
var utf8NoBom = new UTF8Encoding(false);
await File.WriteAllTextAsync(serviceJsPath, serviceJsContent, utf8NoBom);
}
}
}