Added WebMods feature

- Added WebMods feature, allows loading custom js and css
This commit is contained in:
Zarg 2025-10-08 07:25:29 +02:00
parent c5a6a7fb64
commit 7add3f622b
8 changed files with 307 additions and 140 deletions

View file

@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.16)
project(stremio VERSION "5.0.19")
project(stremio VERSION "5.0.20")
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")

View file

@ -27,7 +27,7 @@ using json = nlohmann::json;
#define APP_TITLE "Stremio - Freedom to Stream"
#define APP_NAME "Stremio"
#define APP_CLASS L"Stremio"
#define APP_VERSION "5.0.19"
#define APP_VERSION "5.0.20"
// -----------------------------------------------------------------------------
// Globals

View file

@ -1,178 +1,176 @@
#pragma comment(linker, "/SUBSYSTEM:WINDOWS")
#pragma comment(linker, "/ENTRY:mainCRTStartup")
#include "discord_rpc.h"
#include <windows.h>
#include <shellscalingapi.h>
#include <VersionHelpers.h>
#include <gdiplus.h>
#include <iostream>
#include <shellscalingapi.h>
#include <sstream>
#include <VersionHelpers.h>
#include "discord_rpc.h"
#include "ui/mainwindow.h"
#include "core/globals.h"
#include "utils/crashlog.h"
#include "utils/config.h"
#include "tray/tray.h"
#include "mpv/player.h"
#include "node/server.h"
#include "tray/tray.h"
#include "ui/mainwindow.h"
#include "ui/splash.h"
#include "webview/webview.h"
#include "updater/updater.h"
#include "utils/helpers.h"
#include "utils/config.h"
#include "utils/crashlog.h"
#include "utils/discord.h"
#include "utils/helpers.h"
#include "webview/webview.h"
// This started as 1-week project so please don't take the code to seriously
int main(int argc, char* argv[])
{
// Catch unhandled exceptions
SetUnhandledExceptionFilter([](EXCEPTION_POINTERS* info) -> LONG
{
std::wstringstream ws;
ws << L"Unhandled exception! Code=0x" << std::hex << info->ExceptionRecord->ExceptionCode;
AppendToCrashLog(ws.str());
Cleanup();
return EXCEPTION_EXECUTE_HANDLER;
});
atexit(Cleanup);
int main(int argc, char *argv[]) {
// Catch unhandled exceptions
SetUnhandledExceptionFilter([](EXCEPTION_POINTERS *info) -> LONG {
std::wstringstream ws;
ws << L"Unhandled exception! Code=0x" << std::hex
<< info->ExceptionRecord->ExceptionCode;
AppendToCrashLog(ws.str());
Cleanup();
return EXCEPTION_EXECUTE_HANDLER;
});
atexit(Cleanup);
// DPI
if (IsWindowsVersionOrGreater(10, 0, 14393)){
typedef BOOL(WINAPI *SetDpiCtxFn)(DPI_AWARENESS_CONTEXT);
auto setDpiAwarenessContext = (SetDpiCtxFn)GetProcAddress(GetModuleHandleW(L"user32.dll"), "SetProcessDpiAwarenessContext");
if(setDpiAwarenessContext){
setDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
} else {
// Fallback for Windows 8.1 and Windows 10 before 1607:
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
// DPI
if (IsWindowsVersionOrGreater(10, 0, 14393)) {
typedef BOOL(WINAPI * SetDpiCtxFn)(DPI_AWARENESS_CONTEXT);
auto setDpiAwarenessContext = (SetDpiCtxFn)GetProcAddress(
GetModuleHandleW(L"user32.dll"), "SetProcessDpiAwarenessContext");
if (setDpiAwarenessContext) {
setDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
} else {
// Fallback for Windows 8.1 and Windows 10 before 1607:
SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
}
// parse cmd line
for(int i=1; i<argc; i++){
std::string arg(argv[i]);
if(arg.rfind("--webui-url=", 0)==0){
g_webuiUrls.insert(g_webuiUrls.begin(), Utf8ToWstring(arg.substr(12)));
} else if(arg.rfind("--autoupdater-endpoint=",0)==0){
g_updateUrl = arg.substr(23);
} else if(arg=="--streaming-server-disabled"){
g_streamingServer=false;
} else if(arg=="--autoupdater-force-full"){
g_autoupdaterForceFull=true;
}
// parse cmd line
for (int i = 1; i < argc; i++) {
std::string arg(argv[i]);
if (arg.rfind("--webui-url=", 0) == 0) {
g_webuiUrls.insert(g_webuiUrls.begin(), Utf8ToWstring(arg.substr(12)));
} else if (arg.rfind("--autoupdater-endpoint=", 0) == 0) {
g_updateUrl = arg.substr(23);
} else if (arg == "--streaming-server-disabled") {
g_streamingServer = false;
} else if (arg == "--autoupdater-force-full") {
g_autoupdaterForceFull = true;
}
}
// single instance
std::wstring launchProtocol;
if(!CheckSingleInstance(argc, argv, launchProtocol)){
return 0;
}
g_launchProtocol = launchProtocol;
// single instance
std::wstring launchProtocol;
if (!CheckSingleInstance(argc, argv, launchProtocol)) {
return 0;
}
g_launchProtocol = launchProtocol;
// check stremio-runtime duplicates
std::vector<std::wstring> processesToCheck={L"stremio.exe", L"stremio-runtime.exe"};
if(IsDuplicateProcessRunning(processesToCheck)){
MessageBoxW(nullptr,
L"An older version of Stremio or Stremio server may be running. There could be issues.",
L"Stremio Already Running", MB_OK|MB_ICONWARNING);
}
// check stremio-runtime duplicates
std::vector<std::wstring> processesToCheck = {L"stremio.exe",
L"stremio-runtime.exe"};
if (IsDuplicateProcessRunning(processesToCheck)) {
MessageBoxW(nullptr,
L"An older version of Stremio or Stremio server may be "
L"running. There could be issues.",
L"Stremio Already Running", MB_OK | MB_ICONWARNING);
}
// init GDI+
Gdiplus::GdiplusStartupInput gpsi;
if(Gdiplus::GdiplusStartup(&g_gdiplusToken, &gpsi, nullptr)!=Gdiplus::Ok){
AppendToCrashLog(L"[BOOT]: GdiplusStartup failed.");
return 1;
}
// init GDI+
Gdiplus::GdiplusStartupInput gpsi;
if (Gdiplus::GdiplusStartup(&g_gdiplusToken, &gpsi, nullptr) != Gdiplus::Ok) {
AppendToCrashLog(L"[BOOT]: GdiplusStartup failed.");
return 1;
}
// Load config
LoadSettings();
// Load config
LoadSettings();
// Initialize Discord RPC
InitializeDiscord();
// Initialize Discord RPC
InitializeDiscord();
// Updater
g_updaterThread=std::thread(RunAutoUpdaterOnce);
g_updaterThread.detach();
// Updater
g_updaterThread = std::thread(RunAutoUpdaterOnce);
g_updaterThread.detach();
g_hInst = GetModuleHandle(nullptr);
g_darkBrush = CreateSolidBrush(RGB(0,0,0));
g_hInst = GetModuleHandle(nullptr);
g_darkBrush = CreateSolidBrush(RGB(0, 0, 0));
// Register main window class
WNDCLASSEX wcex={0};
wcex.cbSize=sizeof(WNDCLASSEX);
wcex.style=CS_HREDRAW|CS_VREDRAW;
wcex.lpfnWndProc=WndProc;
wcex.hInstance=g_hInst;
wcex.hCursor=LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground=g_darkBrush;
wcex.lpszClassName=szWindowClass;
if(!RegisterClassEx(&wcex)){
AppendToCrashLog(L"[BOOT]: RegisterClassEx failed!");
return 1;
}
// Register main window class
WNDCLASSEX wcex = {0};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.hInstance = g_hInst;
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = g_darkBrush;
wcex.lpszClassName = szWindowClass;
if (!RegisterClassEx(&wcex)) {
AppendToCrashLog(L"[BOOT]: RegisterClassEx failed!");
return 1;
}
g_hWnd = CreateWindow(szWindowClass, szTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
1200, 900,
nullptr, nullptr,
g_hInst, nullptr);
if(!g_hWnd){
AppendToCrashLog(L"[BOOT]: CreateWindow failed!");
return 1;
}
g_hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 1200, 900, nullptr,
nullptr, g_hInst, nullptr);
if (!g_hWnd) {
AppendToCrashLog(L"[BOOT]: CreateWindow failed!");
return 1;
}
//Add PlayPause Hotkey
if (!RegisterHotKey(g_hWnd, 1, 0, VK_MEDIA_PLAY_PAUSE)) {
AppendToCrashLog(L"[BOOT]: Failed to register hotkey!");
}
// Add PlayPause Hotkey
if (!RegisterHotKey(g_hWnd, 1, 0, VK_MEDIA_PLAY_PAUSE)) {
AppendToCrashLog(L"[BOOT]: Failed to register hotkey!");
}
// Scale Values with DPI
ScaleWithDPI();
LoadCustomMenuFont();
// Scale Values with DPI
ScaleWithDPI();
LoadCustomMenuFont();
// Load Saved position
WINDOWPLACEMENT wp;
if (LoadWindowPlacement(wp)) {
SetWindowPlacement(g_hWnd, &wp);
ShowWindow(g_hWnd, wp.showCmd);
UpdateWindow(g_hWnd);
} else {
ShowWindow(g_hWnd,SW_SHOW);
UpdateWindow(g_hWnd);
}
// Load Saved position
WINDOWPLACEMENT wp;
if (LoadWindowPlacement(wp)) {
SetWindowPlacement(g_hWnd, &wp);
ShowWindow(g_hWnd, wp.showCmd);
UpdateWindow(g_hWnd);
} else {
ShowWindow(g_hWnd, SW_SHOW);
UpdateWindow(g_hWnd);
}
// create splash
CreateSplashScreen(g_hWnd);
// create splash
CreateSplashScreen(g_hWnd);
// init mpv
if(!InitMPV(g_hWnd)){
DestroyWindow(g_hWnd);
return 1;
}
// init mpv
if (!InitMPV(g_hWnd)) {
DestroyWindow(g_hWnd);
return 1;
}
// node
if(g_streamingServer){
StartNodeServer();
}
// node
if (g_streamingServer) {
StartNodeServer();
}
// webview
InitWebView2(g_hWnd);
// webview
InitWebView2(g_hWnd);
// message loop
MSG msg;
while(GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
// message loop
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
// Run Discord RPC callbacks
Discord_RunCallbacks();
}
// Run Discord RPC callbacks
Discord_RunCallbacks();
}
if(g_darkBrush){
DeleteObject(g_darkBrush);
g_darkBrush=nullptr;
}
std::cout<<"Exiting...\n";
return (int)msg.wParam;
if (g_darkBrush) {
DeleteObject(g_darkBrush);
g_darkBrush = nullptr;
}
std::cout << "Exiting...\n";
return (int)msg.wParam;
}

View file

@ -1,5 +1,6 @@
#include "helpers.h"
#include <fstream>
#include <iostream>
#include <shellscalingapi.h>
#include <tlhelp32.h>
@ -244,4 +245,104 @@ void ScaleWithDPI() {
g_tray_sepH = ScaleValue(g_tray_sepH);
g_tray_w = ScaleValue(g_tray_w);
g_font_height = ScaleValue(g_font_height);
}
// ---- UTF-8 file read
bool ReadFileUtf8(const std::wstring& path, std::string& out)
{
std::ifstream f(path, std::ios::binary);
if (!f) return false;
f.seekg(0, std::ios::end);
std::streamsize size = f.tellg();
f.seekg(0, std::ios::beg);
out.resize(static_cast<size_t>(size));
if (size > 0) f.read(&out[0], size);
return true;
}
// ---- tiny Base64
std::string Base64Encode(const std::string& in)
{
static const char* T = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
out.reserve(((in.size() + 2) / 3) * 4);
int val = 0, valb = -6;
for (uint8_t c : in) {
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back(T[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back(T[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}
// ---- wrap CSS text into a safe injector (decodes UTF-8 from base64)
std::wstring MakeInjectCssScript(const std::wstring& idSafe, const std::string& cssUtf8)
{
const std::string b64 = Base64Encode(cssUtf8);
const std::wstring wb64 = Utf8ToWstring(b64);
std::wstringstream ss;
ss <<
L"(function(){try{"
L"if(window.top!==window)return;"
L"var id='webmods-css-" << idSafe << L"';"
L"function inject(){"
L"try{"
L"var root=document.head||document.documentElement||document.body;"
L"if(!root){"
L"document.addEventListener('DOMContentLoaded',inject,{once:true});"
L"document.addEventListener('readystatechange',function(){"
L"if(document.readyState==='interactive'||document.readyState==='complete')inject();"
L"},{once:true});"
L"setTimeout(inject,25);"
L"return;"
L"}"
L"if(document.getElementById(id))return;"
L"var bin=atob('" << wb64 << L"');"
L"var bytes=new Uint8Array(bin.length);"
L"for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);"
L"var css='';"
L"try{css=new TextDecoder('utf-8').decode(bytes);}catch(e){css=decodeURIComponent(escape(bin));}"
L"var s=document.createElement('style');"
L"s.id=id;"
L"s.textContent=css;"
L"root.appendChild(s);"
L"}catch(e){console.error('webmods css inject tick failed:',e);setTimeout(inject,50);}"
L"}"
L"inject();"
L"}catch(e){console.error('webmods css inject failed:',e);}})();";
return ss.str();
}
// ---- wrap JS text into a safe executor
std::wstring MakeInjectJsScript(const std::wstring&,const std::string& jsUtf8)
{
const std::string b64 = Base64Encode(jsUtf8);
const std::wstring wb64 = Utf8ToWstring(b64);
std::wstringstream ss;
ss <<
L"(function(){try{"
L"if(window.top!==window)return;"
L"function run(){"
L"try{"
L"var bin=atob('" << wb64 << L"');"
L"var bytes=new Uint8Array(bin.length);"
L"for(var i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);"
L"var js='';"
L"try{js=new TextDecoder('utf-8').decode(bytes);}catch(e){js=decodeURIComponent(escape(bin));}"
L"(0,eval)(js);"
L"}catch(e){console.error('webmods js exec tick failed:',e);setTimeout(run,25);}"
L"}"
L"run();"
L"}catch(e){console.error('webmods js exec failed:',e);}})();";
return ss.str();
}

View file

@ -11,6 +11,8 @@ std::wstring Utf8ToWstring(const std::string& utf8Str);
std::string decodeURIComponent(const std::string& encoded);
std::wstring GetExeDirectory();
std::wstring GetFirstReachableUrl();
std::wstring MakeInjectCssScript(const std::wstring& idSafe, const std::string& cssUtf8);
std::wstring MakeInjectJsScript(const std::wstring& idSafe, const std::string& jsUtf8);
bool FileExists(const std::wstring& path);
bool DirectoryExists(const std::wstring& dirPath);
bool IsDuplicateProcessRunning(const std::vector<std::wstring>& targetProcesses);
@ -18,5 +20,6 @@ bool isSubtitle(const std::wstring& filePath);
bool URLContainsAny(const std::wstring& url);
bool FetchAndParseWhitelist();
void ScaleWithDPI();
bool ReadFileUtf8(const std::wstring& path, std::string& out);
#endif // HELPERS_H

View file

@ -289,6 +289,8 @@ void InitWebView2(HWND hWnd)
g_webview->AddScriptToExecuteOnDocumentCreated(EXEC_SHELL_SCRIPT,nullptr);
g_webview->AddScriptToExecuteOnDocumentCreated(INJECTED_KEYDOWN_SCRIPT,nullptr);
SetupWebMods();
SetupExtensions();
SetupWebMessageHandler();
@ -627,6 +629,68 @@ static void SetupExtensions()
}
}
static void SetupWebMods()
{
if (!g_webview) return;
wchar_t buf[MAX_PATH];
GetModuleFileNameW(nullptr, buf, MAX_PATH);
std::wstring exeDir = buf;
size_t pos = exeDir.find_last_of(L"\\/");
if (pos != std::wstring::npos) exeDir.erase(pos);
const std::filesystem::path root = std::filesystem::path(exeDir) / L"portable_config" / L"webmods";
if (!std::filesystem::exists(root) || !std::filesystem::is_directory(root)) {
std::wcout << L"[WEBMODS] Folder not found: " << root.wstring() << std::endl;
return;
}
std::vector<std::filesystem::path> cssFiles, jsFiles;
for (const auto& e : std::filesystem::recursive_directory_iterator(root)) {
if (!e.is_regular_file()) continue;
auto ext = e.path().extension().wstring();
std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower);
if (ext == L".map" || ext == L".bak" || ext == L".tmp") continue;
if (ext == L".css") cssFiles.push_back(e.path());
else if (ext == L".js") jsFiles.push_back(e.path());
}
auto relStr = [&](const std::filesystem::path& p){
try { return std::filesystem::relative(p, root).wstring(); }
catch(...) { return p.wstring(); }
};
auto sorter = [&](const std::filesystem::path& a, const std::filesystem::path& b){
auto ra = relStr(a), rb = relStr(b);
return _wcsicmp(ra.c_str(), rb.c_str()) < 0;
};
std::sort(cssFiles.begin(), cssFiles.end(), sorter);
std::sort(jsFiles.begin(), jsFiles.end(), sorter);
auto makeId = [&](const std::filesystem::path& p){
std::wstring id = relStr(p);
for (auto& ch : id) if (!iswalnum(ch)) ch = L'_';
return id;
};
for (const auto& p : cssFiles) {
std::string content;
if (!ReadFileUtf8(p.wstring(), content)) continue;
const std::wstring id = makeId(p);
const std::wstring script = MakeInjectCssScript(id, content);
g_webview->AddScriptToExecuteOnDocumentCreated(script.c_str(), nullptr);
std::wcout << L"[WEBMODS] CSS: " << relStr(p) << std::endl;
}
for (const auto& p : jsFiles) {
std::string content;
if (!ReadFileUtf8(p.wstring(), content)) continue;
const std::wstring id = makeId(p);
const std::wstring script = MakeInjectJsScript(id, content);
g_webview->AddScriptToExecuteOnDocumentCreated(script.c_str(), nullptr);
std::wcout << L"[WEBMODS] JS: " << relStr(p) << std::endl;
}
}
void refreshWeb(const bool refreshAll) {
if (g_webviewProfile && refreshAll)
{

View file

@ -9,5 +9,6 @@ void WaitAndRefreshIfNeeded();
void refreshWeb(bool refreshAll);
static void SetupWebMessageHandler();
static void SetupExtensions();
static void SetupWebMods();
#endif // WEBVIEW_H

Binary file not shown.