mirror of
https://github.com/Jellyfin2Samsung/Samsung-Jellyfin-Installer.git
synced 2026-03-01 11:21:12 +03:00
add esbuild - my bad sorry
beta build service for youtube
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -178,7 +178,6 @@ DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
esbuild/
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
|
||||
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/linux-x64/esbuild
Normal file
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/linux-x64/esbuild
Normal file
Binary file not shown.
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/macos-x64/esbuild
Normal file
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/macos-x64/esbuild
Normal file
Binary file not shown.
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/win-x64/esbuild.exe
Normal file
BIN
Jellyfin2Samsung-CrossOS/Assets/esbuild/win-x64/esbuild.exe
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user