Initial commit: stools Plugin Manager for OBS
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
||||
build/
|
||||
deps/
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.obj
|
||||
*.lib
|
||||
*.exp
|
||||
*.pdb
|
||||
*.ilk
|
||||
.vs/
|
||||
.vscode/
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
cmake_minimum_required(VERSION 3.16...3.28)
|
||||
|
||||
project(st-pluginmanager VERSION 1.0.0 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC OFF)
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE RelWithDebInfo)
|
||||
endif()
|
||||
|
||||
add_library(st-pluginmanager MODULE
|
||||
src/plugin-main.c
|
||||
src/auth.c
|
||||
src/downloader.c
|
||||
src/obfuscation.cpp
|
||||
src/manager-dialog.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
set(OBS_SOURCE_DIR "" CACHE PATH "Path to obs-studio source tree")
|
||||
set(OBS_LIB_DIR "" CACHE PATH "Path to OBS import libraries")
|
||||
set(QT6_DIR "" CACHE PATH "Path to Qt6 dev package")
|
||||
|
||||
add_library(obs_lib SHARED IMPORTED)
|
||||
set_target_properties(obs_lib PROPERTIES
|
||||
IMPORTED_IMPLIB "${OBS_LIB_DIR}/obs.lib"
|
||||
)
|
||||
|
||||
add_library(obs_frontend SHARED IMPORTED)
|
||||
set_target_properties(obs_frontend PROPERTIES
|
||||
IMPORTED_IMPLIB "${OBS_LIB_DIR}/obs-frontend-api.lib"
|
||||
)
|
||||
|
||||
add_library(w32_pthreads SHARED IMPORTED)
|
||||
set_target_properties(w32_pthreads PROPERTIES
|
||||
IMPORTED_IMPLIB "${OBS_LIB_DIR}/w32-pthreads.lib"
|
||||
)
|
||||
|
||||
foreach(_qt_mod Core Gui Widgets)
|
||||
add_library(Qt6::${_qt_mod} SHARED IMPORTED)
|
||||
set_target_properties(Qt6::${_qt_mod} PROPERTIES
|
||||
IMPORTED_IMPLIB "${QT6_DIR}/lib/Qt6${_qt_mod}.lib"
|
||||
)
|
||||
endforeach()
|
||||
|
||||
target_include_directories(st-pluginmanager PRIVATE
|
||||
"${OBS_SOURCE_DIR}/libobs"
|
||||
"${OBS_SOURCE_DIR}/frontend/api"
|
||||
"${OBS_SOURCE_DIR}/deps/w32-pthreads"
|
||||
"${QT6_DIR}/include"
|
||||
"${QT6_DIR}/include/QtCore"
|
||||
"${QT6_DIR}/include/QtGui"
|
||||
"${QT6_DIR}/include/QtWidgets"
|
||||
)
|
||||
|
||||
# Find curl from obs-deps or system
|
||||
set(CURL_DIR "" CACHE PATH "Path to curl dev package (include + lib)")
|
||||
if(CURL_DIR)
|
||||
target_include_directories(st-pluginmanager PRIVATE "${CURL_DIR}/include")
|
||||
set(_CURL_LIB "${CURL_DIR}/lib/libcurl_imp.lib")
|
||||
else()
|
||||
set(_CURL_LIB "libcurl_imp.lib")
|
||||
endif()
|
||||
|
||||
target_link_libraries(st-pluginmanager
|
||||
obs_lib
|
||||
obs_frontend
|
||||
w32_pthreads
|
||||
shell32
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
"${_CURL_LIB}"
|
||||
)
|
||||
|
||||
target_compile_definitions(st-pluginmanager PRIVATE _CRT_SECURE_NO_WARNINGS)
|
||||
target_compile_options(st-pluginmanager PRIVATE /Zc:__cplusplus /permissive- /utf-8)
|
||||
|
||||
else()
|
||||
find_package(PkgConfig REQUIRED)
|
||||
|
||||
pkg_check_modules(LIBOBS REQUIRED IMPORTED_TARGET libobs)
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)
|
||||
find_package(CURL REQUIRED)
|
||||
|
||||
pkg_check_modules(OBS_FRONTEND IMPORTED_TARGET obs-frontend-api)
|
||||
if(OBS_FRONTEND_FOUND)
|
||||
set(_OBS_FRONTEND_TARGET PkgConfig::OBS_FRONTEND)
|
||||
else()
|
||||
find_library(OBS_FRONTEND_LIB obs-frontend-api)
|
||||
if(OBS_FRONTEND_LIB)
|
||||
add_library(obs_frontend_imported SHARED IMPORTED)
|
||||
set_target_properties(obs_frontend_imported PROPERTIES
|
||||
IMPORTED_LOCATION "${OBS_FRONTEND_LIB}"
|
||||
)
|
||||
set(_OBS_FRONTEND_TARGET obs_frontend_imported)
|
||||
else()
|
||||
set(_OBS_FRONTEND_TARGET obs-frontend-api)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(st-pluginmanager
|
||||
PkgConfig::LIBOBS
|
||||
${_OBS_FRONTEND_TARGET}
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
CURL::libcurl
|
||||
pthread
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS st-pluginmanager
|
||||
LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}/obs-plugins")
|
||||
endif()
|
||||
|
||||
if(DEFINED PLUGIN_VERSION_OVERRIDE)
|
||||
set(_PLUGIN_VER "${PLUGIN_VERSION_OVERRIDE}")
|
||||
else()
|
||||
set(_PLUGIN_VER "${PROJECT_VERSION}")
|
||||
endif()
|
||||
|
||||
option(DEBUG_BUILD "Enable debug logging" OFF)
|
||||
|
||||
target_compile_definitions(st-pluginmanager PRIVATE
|
||||
PLUGIN_VERSION="${_PLUGIN_VER}"
|
||||
$<$<BOOL:${DEBUG_BUILD}>:DEBUG_BUILD>
|
||||
)
|
||||
|
||||
set_target_properties(st-pluginmanager PROPERTIES PREFIX "")
|
||||
@@ -0,0 +1,280 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Lesser General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
@@ -0,0 +1,22 @@
|
||||
# stools Plugin Manager
|
||||
|
||||
OBS Studio plugin that manages stools plugins. Automatically downloads, installs and updates plugins with login authentication.
|
||||
|
||||
## Features
|
||||
|
||||
- Login via stools.cc API token
|
||||
- Automatic plugin download and installation
|
||||
- Auto-update check on OBS startup
|
||||
- Plugin version tracking
|
||||
- Cross-platform (Windows, macOS, Linux)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
# Windows (requires Visual Studio + OBS installed)
|
||||
powershell -ExecutionPolicy Bypass -File build.ps1
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [GNU General Public License v2.0](LICENSE).
|
||||
@@ -0,0 +1,115 @@
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ROOT = $PSScriptRoot
|
||||
|
||||
# --- VS Developer Environment ---
|
||||
$vsPath = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath
|
||||
if (-not $vsPath) { throw "Visual Studio not found" }
|
||||
|
||||
Import-Module "$vsPath\Common7\Tools\Microsoft.VisualStudio.DevShell.dll"
|
||||
Enter-VsDevShell -VsInstallPath $vsPath -DevCmdArguments "-arch=x64 -host_arch=x64" -SkipAutomaticLocation
|
||||
|
||||
$OBS_BIN = "C:\Program Files\obs-studio\bin\64bit"
|
||||
$DEPS_DIR = "$ROOT\deps"
|
||||
$OBS_SRC = "$DEPS_DIR\obs-studio"
|
||||
$OBS_DEPS = "$DEPS_DIR\obs-deps"
|
||||
$QT6_DIR = "$DEPS_DIR\qt6"
|
||||
$OBS_LIBS = "$DEPS_DIR\obs-libs"
|
||||
$BUILD_DIR = "$ROOT\build"
|
||||
$OBS_VERSION = "32.1.0"
|
||||
|
||||
# --- 1. OBS headers (sparse clone) ---
|
||||
if (-not (Test-Path "$OBS_SRC\libobs\obs-module.h")) {
|
||||
Write-Host "[1/5] Cloning OBS Studio headers..."
|
||||
git clone --depth 1 --branch $OBS_VERSION --filter=blob:none --sparse `
|
||||
"https://github.com/obsproject/obs-studio.git" $OBS_SRC
|
||||
Push-Location $OBS_SRC
|
||||
git sparse-checkout set libobs frontend/api deps/w32-pthreads
|
||||
Pop-Location
|
||||
|
||||
@"
|
||||
#pragma once
|
||||
#define OBS_DATA_PATH "data"
|
||||
#define OBS_PLUGIN_PATH "obs-plugins/64bit"
|
||||
#define OBS_PLUGIN_DESTINATION "obs-plugins/64bit"
|
||||
#define OBS_INSTALL_PREFIX "C:/Program Files/obs-studio"
|
||||
#define OBS_RELEASE_CANDIDATE 0
|
||||
#define OBS_BETA 0
|
||||
"@ | Out-File -Encoding ASCII "$OBS_SRC\libobs\obsconfig.h"
|
||||
} else {
|
||||
Write-Host "[1/5] OBS headers: OK"
|
||||
}
|
||||
|
||||
# --- 2. OBS deps (curl headers + libs) ---
|
||||
$depsZip = "$DEPS_DIR\windows-deps-x64.zip"
|
||||
if (-not (Test-Path "$OBS_DEPS\lib\libcurl_imp.lib")) {
|
||||
Write-Host "[2/5] Downloading OBS deps..."
|
||||
New-Item -ItemType Directory -Force -Path $DEPS_DIR | Out-Null
|
||||
$depsUrl = "https://github.com/obsproject/obs-deps/releases/download/2025-08-23/windows-deps-2025-08-23-x64.zip"
|
||||
Invoke-WebRequest -Uri $depsUrl -OutFile $depsZip -UseBasicParsing
|
||||
Expand-Archive -Path $depsZip -DestinationPath $OBS_DEPS -Force
|
||||
} else {
|
||||
Write-Host "[2/5] OBS deps: OK"
|
||||
}
|
||||
|
||||
# --- 3. Qt6 deps ---
|
||||
$qt6Zip = "$DEPS_DIR\windows-deps-qt6-x64.zip"
|
||||
if (-not (Test-Path "$QT6_DIR\include\QtWidgets")) {
|
||||
Write-Host "[3/5] Downloading Qt6 deps..."
|
||||
New-Item -ItemType Directory -Force -Path $QT6_DIR | Out-Null
|
||||
$qt6Url = "https://github.com/obsproject/obs-deps/releases/download/2025-08-23/windows-deps-qt6-2025-08-23-x64.zip"
|
||||
Invoke-WebRequest -Uri $qt6Url -OutFile $qt6Zip -UseBasicParsing
|
||||
Expand-Archive -Path $qt6Zip -DestinationPath $QT6_DIR -Force
|
||||
} else {
|
||||
Write-Host "[3/5] Qt6 deps: OK"
|
||||
}
|
||||
|
||||
# --- 4. Generate OBS import libraries ---
|
||||
if (-not (Test-Path "$OBS_LIBS\obs.lib") -or (Get-Item "$OBS_LIBS\obs.lib").Length -lt 10000) {
|
||||
Write-Host "[4/5] Generating import libraries..."
|
||||
New-Item -ItemType Directory -Force -Path $OBS_LIBS | Out-Null
|
||||
|
||||
foreach ($name in @("obs", "obs-frontend-api", "w32-pthreads")) {
|
||||
$raw = (& dumpbin /exports "$OBS_BIN\$name.dll" 2>&1) | Out-String
|
||||
$lines = $raw -split "`r?`n"
|
||||
$defLines = @("LIBRARY `"$name`"", "EXPORTS")
|
||||
$capture = $false
|
||||
foreach ($line in $lines) {
|
||||
if ($line -match "ordinal\s+hint\s+RVA\s+name") { $capture = $true; continue }
|
||||
if ($capture -and $line -match "Summary") { break }
|
||||
if ($capture -and $line -match "^\s+(\d+)\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+(\S+)") {
|
||||
$defLines += " $($Matches[2])"
|
||||
}
|
||||
}
|
||||
$defLines -join "`n" | Out-File -Encoding ASCII "$OBS_LIBS\$name.def" -NoNewline
|
||||
& lib /nologo /def:"$OBS_LIBS\$name.def" /out:"$OBS_LIBS\$name.lib" /machine:x64 2>$null | Out-Null
|
||||
}
|
||||
} else {
|
||||
Write-Host "[4/5] Import libraries: OK"
|
||||
}
|
||||
|
||||
# --- 5. Build ---
|
||||
Write-Host "[5/5] Building..."
|
||||
$debugFlag = if ($env:DEBUG_BUILD -eq "1") { "-DDEBUG_BUILD=ON" } else { "-DDEBUG_BUILD=OFF" }
|
||||
|
||||
cmake -S $ROOT -B $BUILD_DIR -G "Ninja" `
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo `
|
||||
-DCMAKE_C_COMPILER=cl `
|
||||
-DCMAKE_CXX_COMPILER=cl `
|
||||
-DOBS_SOURCE_DIR="$OBS_SRC" `
|
||||
-DOBS_LIB_DIR="$OBS_LIBS" `
|
||||
-DCURL_DIR="$OBS_DEPS" `
|
||||
-DQT6_DIR="$QT6_DIR" `
|
||||
$debugFlag
|
||||
|
||||
cmake --build $BUILD_DIR --config RelWithDebInfo
|
||||
|
||||
$dll = "$BUILD_DIR\st-pluginmanager.dll"
|
||||
if (Test-Path $dll) {
|
||||
$size = [math]::Round((Get-Item $dll).Length / 1KB, 1)
|
||||
Write-Host ""
|
||||
Write-Host "BUILD SUCCESSFUL: st-pluginmanager.dll ($size KB)" -ForegroundColor Green
|
||||
Write-Host "Output: $dll"
|
||||
} else {
|
||||
Write-Host "BUILD FAILED" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
#include "auth.h"
|
||||
#include "obfuscation.h"
|
||||
#include "debug-log.h"
|
||||
#include "compat.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <shlobj.h>
|
||||
#else
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#endif
|
||||
|
||||
#define MAX_TOKEN_LEN 512
|
||||
#define MAX_USERNAME_LEN 128
|
||||
|
||||
static char g_token[MAX_TOKEN_LEN] = "";
|
||||
static char g_username[MAX_USERNAME_LEN] = "";
|
||||
static bool g_logged_in = false;
|
||||
|
||||
/* ---- Token file path ---- */
|
||||
|
||||
static void get_token_path(char *buf, size_t sz)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char appdata[MAX_PATH];
|
||||
SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata);
|
||||
snprintf(buf, sz, "%s\\stools\\pluginmanager_token", appdata);
|
||||
#else
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd *pw = getpwuid(getuid());
|
||||
home = pw ? pw->pw_dir : "/tmp";
|
||||
}
|
||||
snprintf(buf, sz, "%s/.config/stools/pluginmanager_token", home);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void ensure_dir(const char *path)
|
||||
{
|
||||
char dir[512];
|
||||
snprintf(dir, sizeof(dir), "%s", path);
|
||||
char *last_sep = strrchr(dir, '/');
|
||||
#ifdef _WIN32
|
||||
char *last_bs = strrchr(dir, '\\');
|
||||
if (last_bs && (!last_sep || last_bs > last_sep))
|
||||
last_sep = last_bs;
|
||||
#endif
|
||||
if (last_sep) {
|
||||
*last_sep = '\0';
|
||||
#ifdef _WIN32
|
||||
CreateDirectoryA(dir, NULL);
|
||||
#else
|
||||
mkdir(dir, 0700);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static void save_token(const char *token)
|
||||
{
|
||||
char path[512];
|
||||
get_token_path(path, sizeof(path));
|
||||
ensure_dir(path);
|
||||
FILE *f = fopen(path, "w");
|
||||
if (f) {
|
||||
fputs(token, f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
|
||||
static bool load_token(char *buf, size_t sz)
|
||||
{
|
||||
char path[512];
|
||||
get_token_path(path, sizeof(path));
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return false;
|
||||
if (!fgets(buf, (int)sz, f)) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
fclose(f);
|
||||
size_t len = strlen(buf);
|
||||
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r'))
|
||||
buf[--len] = '\0';
|
||||
return len > 0;
|
||||
}
|
||||
|
||||
static void delete_token(void)
|
||||
{
|
||||
char path[512];
|
||||
get_token_path(path, sizeof(path));
|
||||
remove(path);
|
||||
}
|
||||
|
||||
/* ---- cURL helpers ---- */
|
||||
|
||||
struct mem_buf {
|
||||
char *data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
static size_t write_cb(void *contents, size_t size, size_t nmemb, void *userp)
|
||||
{
|
||||
size_t total = size * nmemb;
|
||||
struct mem_buf *buf = (struct mem_buf *)userp;
|
||||
char *tmp = (char *)realloc(buf->data, buf->size + total + 1);
|
||||
if (!tmp) return 0;
|
||||
buf->data = tmp;
|
||||
memcpy(buf->data + buf->size, contents, total);
|
||||
buf->size += total;
|
||||
buf->data[buf->size] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
static void curl_set_ssl_opts(CURL *curl)
|
||||
{
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
#ifdef _WIN32
|
||||
curl_easy_setopt(curl, CURLOPT_SSLVERSION,
|
||||
CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_1_1);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4);
|
||||
#endif
|
||||
}
|
||||
|
||||
static char *api_get(const char *path, const char *token)
|
||||
{
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url), "%s%s%s",
|
||||
obf_https_prefix(), obf_stools_host(), path);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
char auth_header[512];
|
||||
snprintf(auth_header, sizeof(auth_header), obf_auth_bearer_fmt(), token);
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
|
||||
char ua[128];
|
||||
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
|
||||
|
||||
struct mem_buf buf = {NULL, 0};
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
|
||||
curl_set_ssl_opts(curl);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK || http_code != 200) {
|
||||
free(buf.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return buf.data;
|
||||
}
|
||||
|
||||
/* ---- JSON helpers ---- */
|
||||
|
||||
static const char *json_find_key(const char *json, const char *key)
|
||||
{
|
||||
char search[256];
|
||||
snprintf(search, sizeof(search), "\"%s\"", key);
|
||||
const char *pos = strstr(json, search);
|
||||
if (!pos) return NULL;
|
||||
pos += strlen(search);
|
||||
while (*pos == ' ' || *pos == ':') pos++;
|
||||
return pos;
|
||||
}
|
||||
|
||||
static char *json_get_string_val(const char *json, const char *key)
|
||||
{
|
||||
const char *v = json_find_key(json, key);
|
||||
if (!v || *v != '"') return NULL;
|
||||
v++;
|
||||
const char *end = strchr(v, '"');
|
||||
if (!end) return NULL;
|
||||
size_t len = (size_t)(end - v);
|
||||
char *result = (char *)malloc(len + 1);
|
||||
memcpy(result, v, len);
|
||||
result[len] = '\0';
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ---- Public API ---- */
|
||||
|
||||
bool auth_validate_token(const char *token)
|
||||
{
|
||||
if (!token || !token[0]) return false;
|
||||
|
||||
char *json = api_get(obf_api_me_path(), token);
|
||||
if (!json) return false;
|
||||
|
||||
char *name = json_get_string_val(json, "username");
|
||||
if (!name)
|
||||
name = json_get_string_val(json, "name");
|
||||
|
||||
free(json);
|
||||
|
||||
if (name) {
|
||||
snprintf(g_username, sizeof(g_username), "%s", name);
|
||||
free(name);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool auth_login(const char *token)
|
||||
{
|
||||
if (!auth_validate_token(token))
|
||||
return false;
|
||||
|
||||
snprintf(g_token, sizeof(g_token), "%s", token);
|
||||
g_logged_in = true;
|
||||
save_token(token);
|
||||
|
||||
dbg_log(LOG_INFO, "[%s] Logged in as %s", PLUGIN_NAME, g_username);
|
||||
return true;
|
||||
}
|
||||
|
||||
void auth_logout(void)
|
||||
{
|
||||
g_token[0] = '\0';
|
||||
g_username[0] = '\0';
|
||||
g_logged_in = false;
|
||||
delete_token();
|
||||
dbg_log(LOG_INFO, "[%s] Logged out", PLUGIN_NAME);
|
||||
}
|
||||
|
||||
void auth_init(void)
|
||||
{
|
||||
char token[MAX_TOKEN_LEN];
|
||||
if (load_token(token, sizeof(token))) {
|
||||
if (auth_validate_token(token)) {
|
||||
snprintf(g_token, sizeof(g_token), "%s", token);
|
||||
g_logged_in = true;
|
||||
dbg_log(LOG_INFO, "[%s] Auto-login as %s",
|
||||
PLUGIN_NAME, g_username);
|
||||
} else {
|
||||
dbg_log(LOG_WARNING,
|
||||
"[%s] Saved token invalid, login required",
|
||||
PLUGIN_NAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void auth_shutdown(void)
|
||||
{
|
||||
/* nothing to clean up */
|
||||
}
|
||||
|
||||
bool auth_is_logged_in(void)
|
||||
{
|
||||
return g_logged_in;
|
||||
}
|
||||
|
||||
const char *auth_get_token(void)
|
||||
{
|
||||
return g_token;
|
||||
}
|
||||
|
||||
const char *auth_get_username(void)
|
||||
{
|
||||
return g_username;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void auth_init(void);
|
||||
void auth_shutdown(void);
|
||||
|
||||
bool auth_is_logged_in(void);
|
||||
const char *auth_get_token(void);
|
||||
const char *auth_get_username(void);
|
||||
|
||||
bool auth_login(const char *token);
|
||||
void auth_logout(void);
|
||||
|
||||
bool auth_validate_token(const char *token);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* Platform compatibility layer replacing OBS internal utilities
|
||||
* (util/platform.h, util/threading.h, bmem.h) with standard C equivalents.
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#endif
|
||||
|
||||
/* ---- Memory (replaces bmem.h) ---- */
|
||||
|
||||
#ifdef _MSC_VER
|
||||
#define portable_strdup _strdup
|
||||
#else
|
||||
#define portable_strdup strdup
|
||||
#endif
|
||||
|
||||
/* ---- Time (replaces util/platform.h os_gettime_ns / os_sleep_ms) ---- */
|
||||
|
||||
static inline uint64_t os_gettime_ns(void)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
static LARGE_INTEGER freq = {0};
|
||||
if (freq.QuadPart == 0)
|
||||
QueryPerformanceFrequency(&freq);
|
||||
LARGE_INTEGER counter;
|
||||
QueryPerformanceCounter(&counter);
|
||||
return (uint64_t)((double)counter.QuadPart / (double)freq.QuadPart *
|
||||
1000000000.0);
|
||||
#else
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
|
||||
#endif
|
||||
}
|
||||
|
||||
static inline void os_sleep_ms(uint32_t ms)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
Sleep(ms);
|
||||
#else
|
||||
usleep((useconds_t)ms * 1000);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ---- Thread naming (replaces util/platform.h os_set_thread_name) ---- */
|
||||
|
||||
static inline void os_set_thread_name(const char *name)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
/* SetThreadDescription available on Windows 10 1607+ */
|
||||
typedef HRESULT(WINAPI * SetThreadDescriptionFunc)(HANDLE, PCWSTR);
|
||||
static SetThreadDescriptionFunc fn = NULL;
|
||||
static bool resolved = false;
|
||||
if (!resolved) {
|
||||
HMODULE mod = GetModuleHandleW(L"kernel32.dll");
|
||||
if (mod)
|
||||
fn = (SetThreadDescriptionFunc)GetProcAddress(
|
||||
mod, "SetThreadDescription");
|
||||
resolved = true;
|
||||
}
|
||||
if (fn) {
|
||||
wchar_t wname[64];
|
||||
MultiByteToWideChar(CP_UTF8, 0, name, -1, wname, 64);
|
||||
fn(GetCurrentThread(), wname);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
pthread_setname_np(name);
|
||||
#else
|
||||
pthread_setname_np(pthread_self(), name);
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ---- Atomics (replaces util/platform.h os_atomic_*) ---- */
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
static inline long os_atomic_set_long(volatile long *ptr, long val)
|
||||
{
|
||||
return InterlockedExchange(ptr, val);
|
||||
}
|
||||
|
||||
static inline long os_atomic_load_long(volatile long *ptr)
|
||||
{
|
||||
return InterlockedCompareExchange(ptr, 0, 0);
|
||||
}
|
||||
|
||||
static inline long os_atomic_exchange_long(volatile long *ptr, long val)
|
||||
{
|
||||
return InterlockedExchange(ptr, val);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static inline long os_atomic_set_long(volatile long *ptr, long val)
|
||||
{
|
||||
return __sync_lock_test_and_set(ptr, val);
|
||||
}
|
||||
|
||||
static inline long os_atomic_load_long(volatile long *ptr)
|
||||
{
|
||||
return __sync_add_and_fetch(ptr, 0);
|
||||
}
|
||||
|
||||
static inline long os_atomic_exchange_long(volatile long *ptr, long val)
|
||||
{
|
||||
return __sync_lock_test_and_set(ptr, val);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#ifndef PLUGIN_NAME
|
||||
#define PLUGIN_NAME "stools Plugin Manager"
|
||||
#endif
|
||||
|
||||
#ifdef DEBUG_BUILD
|
||||
#define dbg_log(level, ...) do { fprintf(stderr, __VA_ARGS__); fprintf(stderr, "\n"); } while (0)
|
||||
#else
|
||||
#define dbg_log(...) ((void)0)
|
||||
#endif
|
||||
|
||||
#ifndef LOG_ERROR
|
||||
#define LOG_ERROR 100
|
||||
#define LOG_WARNING 200
|
||||
#define LOG_INFO 300
|
||||
#define LOG_DEBUG 400
|
||||
#endif
|
||||
@@ -0,0 +1,381 @@
|
||||
#include "downloader.h"
|
||||
#include "obfuscation.h"
|
||||
#include "debug-log.h"
|
||||
#include "compat.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <shlobj.h>
|
||||
#include <direct.h>
|
||||
#define PATH_SEP '\\'
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#define PATH_SEP '/'
|
||||
#endif
|
||||
|
||||
/* ---- cURL helpers ---- */
|
||||
|
||||
struct mem_buf {
|
||||
char *data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
static size_t write_cb(void *contents, size_t size, size_t nmemb, void *userp)
|
||||
{
|
||||
size_t total = size * nmemb;
|
||||
struct mem_buf *buf = (struct mem_buf *)userp;
|
||||
char *tmp = (char *)realloc(buf->data, buf->size + total + 1);
|
||||
if (!tmp) return 0;
|
||||
buf->data = tmp;
|
||||
memcpy(buf->data + buf->size, contents, total);
|
||||
buf->size += total;
|
||||
buf->data[buf->size] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
static size_t write_file_cb(void *contents, size_t size, size_t nmemb,
|
||||
void *userp)
|
||||
{
|
||||
FILE *f = (FILE *)userp;
|
||||
return fwrite(contents, size, nmemb, f);
|
||||
}
|
||||
|
||||
static void curl_set_ssl_opts(CURL *curl)
|
||||
{
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
||||
#ifdef _WIN32
|
||||
curl_easy_setopt(curl, CURLOPT_SSLVERSION,
|
||||
CURL_SSLVERSION_TLSv1_2 | CURL_SSLVERSION_MAX_TLSv1_2);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_OPTIONS, (long)CURLSSLOPT_NO_REVOKE);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_1_1);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_ENABLE_ALPN, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_IPRESOLVE, (long)CURL_IPRESOLVE_V4);
|
||||
#endif
|
||||
}
|
||||
|
||||
static char *api_get(const char *path, const char *token)
|
||||
{
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url), "%s%s%s",
|
||||
obf_https_prefix(), obf_stools_host(), path);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
char auth_header[512];
|
||||
snprintf(auth_header, sizeof(auth_header), obf_auth_bearer_fmt(), token);
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
|
||||
char ua[128];
|
||||
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
|
||||
|
||||
struct mem_buf buf = {NULL, 0};
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
|
||||
curl_set_ssl_opts(curl);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (res != CURLE_OK || http_code != 200) {
|
||||
dbg_log(LOG_WARNING, "[%s] API GET %s: %s (HTTP %ld)",
|
||||
PLUGIN_NAME, path,
|
||||
res != CURLE_OK ? curl_easy_strerror(res) : "error",
|
||||
http_code);
|
||||
free(buf.data);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return buf.data;
|
||||
}
|
||||
|
||||
/* ---- JSON helpers ---- */
|
||||
|
||||
static const char *json_find_key(const char *json, const char *key)
|
||||
{
|
||||
char search[256];
|
||||
snprintf(search, sizeof(search), "\"%s\"", key);
|
||||
const char *pos = strstr(json, search);
|
||||
if (!pos) return NULL;
|
||||
pos += strlen(search);
|
||||
while (*pos == ' ' || *pos == ':') pos++;
|
||||
return pos;
|
||||
}
|
||||
|
||||
static void json_extract_string(const char *json, const char *key,
|
||||
char *out, size_t out_sz)
|
||||
{
|
||||
out[0] = '\0';
|
||||
const char *v = json_find_key(json, key);
|
||||
if (!v || *v != '"') return;
|
||||
v++;
|
||||
const char *end = strchr(v, '"');
|
||||
if (!end) return;
|
||||
size_t len = (size_t)(end - v);
|
||||
if (len >= out_sz) len = out_sz - 1;
|
||||
memcpy(out, v, len);
|
||||
out[len] = '\0';
|
||||
}
|
||||
|
||||
/* ---- OBS plugin directory ---- */
|
||||
|
||||
bool downloader_get_obs_plugin_dir(char *buf, size_t sz)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
char appdata[MAX_PATH];
|
||||
SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata);
|
||||
snprintf(buf, sz, "%s\\obs-studio\\obs-plugins\\64bit", appdata);
|
||||
CreateDirectoryA(buf, NULL);
|
||||
return true;
|
||||
#elif defined(__APPLE__)
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) return false;
|
||||
snprintf(buf, sz, "%s/Library/Application Support/obs-studio/obs-plugins",
|
||||
home);
|
||||
mkdir(buf, 0755);
|
||||
return true;
|
||||
#else
|
||||
const char *home = getenv("HOME");
|
||||
if (!home) return false;
|
||||
snprintf(buf, sz, "%s/.config/obs-studio/plugins", home);
|
||||
mkdir(buf, 0755);
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
/* ---- Parse plugin list from API ---- */
|
||||
|
||||
bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
|
||||
char *json = api_get(obf_api_plugins_path(), token);
|
||||
if (!json) return false;
|
||||
|
||||
/*
|
||||
* Expected JSON format:
|
||||
* [{"slug":"easy-irl-stream","name":"Easy IRL Stream",
|
||||
* "version":"1.1.4","platform":{"windows":".dll","linux":".so"}}, ...]
|
||||
*/
|
||||
const char *pos = json;
|
||||
while (out->count < MAX_PLUGINS) {
|
||||
const char *obj_start = strchr(pos, '{');
|
||||
if (!obj_start) break;
|
||||
|
||||
const char *obj_end = strchr(obj_start, '}');
|
||||
if (!obj_end) break;
|
||||
|
||||
size_t obj_len = (size_t)(obj_end - obj_start + 1);
|
||||
char obj_buf[2048];
|
||||
if (obj_len >= sizeof(obj_buf)) {
|
||||
pos = obj_end + 1;
|
||||
continue;
|
||||
}
|
||||
memcpy(obj_buf, obj_start, obj_len);
|
||||
obj_buf[obj_len] = '\0';
|
||||
|
||||
struct plugin_info *pi = &out->items[out->count];
|
||||
json_extract_string(obj_buf, "slug", pi->slug, sizeof(pi->slug));
|
||||
json_extract_string(obj_buf, "name", pi->name, sizeof(pi->name));
|
||||
json_extract_string(obj_buf, "version", pi->latest_version,
|
||||
sizeof(pi->latest_version));
|
||||
|
||||
if (pi->slug[0] && pi->name[0])
|
||||
out->count++;
|
||||
|
||||
pos = obj_end + 1;
|
||||
}
|
||||
|
||||
free(json);
|
||||
dbg_log(LOG_INFO, "[%s] Fetched %d plugins", PLUGIN_NAME, out->count);
|
||||
return out->count > 0;
|
||||
}
|
||||
|
||||
/* ---- Detect installed plugins ---- */
|
||||
|
||||
static bool file_exists(const char *path)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
DWORD attrs = GetFileAttributesA(path);
|
||||
return attrs != INVALID_FILE_ATTRIBUTES;
|
||||
#else
|
||||
return access(path, F_OK) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool read_version_file(const char *dir, const char *slug,
|
||||
char *ver, size_t ver_sz)
|
||||
{
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s%c%s.version", dir, PATH_SEP, slug);
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) return false;
|
||||
if (!fgets(ver, (int)ver_sz, f)) {
|
||||
fclose(f);
|
||||
return false;
|
||||
}
|
||||
fclose(f);
|
||||
size_t len = strlen(ver);
|
||||
while (len > 0 && (ver[len - 1] == '\n' || ver[len - 1] == '\r'))
|
||||
ver[--len] = '\0';
|
||||
return len > 0;
|
||||
}
|
||||
|
||||
static int compare_versions(const char *a, const char *b)
|
||||
{
|
||||
int a1 = 0, a2 = 0, a3 = 0, b1 = 0, b2 = 0, b3 = 0;
|
||||
sscanf(a, "%d.%d.%d", &a1, &a2, &a3);
|
||||
sscanf(b, "%d.%d.%d", &b1, &b2, &b3);
|
||||
if (a1 != b1) return a1 - b1;
|
||||
if (a2 != b2) return a2 - b2;
|
||||
return a3 - b3;
|
||||
}
|
||||
|
||||
void downloader_detect_installed(struct plugin_list *list,
|
||||
const char *obs_plugin_dir)
|
||||
{
|
||||
for (int i = 0; i < list->count; i++) {
|
||||
struct plugin_info *pi = &list->items[i];
|
||||
|
||||
char dll_path[512];
|
||||
#ifdef _WIN32
|
||||
snprintf(dll_path, sizeof(dll_path), "%s\\%s.dll",
|
||||
obs_plugin_dir, pi->slug);
|
||||
#elif defined(__APPLE__)
|
||||
snprintf(dll_path, sizeof(dll_path), "%s/%s.so",
|
||||
obs_plugin_dir, pi->slug);
|
||||
#else
|
||||
snprintf(dll_path, sizeof(dll_path), "%s/%s.so",
|
||||
obs_plugin_dir, pi->slug);
|
||||
#endif
|
||||
|
||||
pi->installed = file_exists(dll_path);
|
||||
|
||||
if (pi->installed) {
|
||||
if (read_version_file(obs_plugin_dir, pi->slug,
|
||||
pi->installed_version,
|
||||
sizeof(pi->installed_version))) {
|
||||
pi->update_available =
|
||||
compare_versions(pi->latest_version,
|
||||
pi->installed_version) > 0;
|
||||
} else {
|
||||
snprintf(pi->installed_version,
|
||||
sizeof(pi->installed_version), "?");
|
||||
pi->update_available = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- Download and install a plugin ---- */
|
||||
|
||||
bool downloader_install_plugin(const char *token, const char *slug,
|
||||
const char *obs_plugin_dir)
|
||||
{
|
||||
const char *platform =
|
||||
#ifdef _WIN32
|
||||
"windows";
|
||||
#elif defined(__APPLE__)
|
||||
"macos";
|
||||
#else
|
||||
"linux";
|
||||
#endif
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), obf_api_plugin_download_fmt(),
|
||||
slug, platform);
|
||||
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url), "%s%s%s",
|
||||
obf_https_prefix(), obf_stools_host(), path);
|
||||
|
||||
const char *ext =
|
||||
#ifdef _WIN32
|
||||
".dll";
|
||||
#else
|
||||
".so";
|
||||
#endif
|
||||
|
||||
char tmp_path[512], final_path[512], ver_path[512];
|
||||
snprintf(tmp_path, sizeof(tmp_path), "%s%c%s%s.tmp",
|
||||
obs_plugin_dir, PATH_SEP, slug, ext);
|
||||
snprintf(final_path, sizeof(final_path), "%s%c%s%s",
|
||||
obs_plugin_dir, PATH_SEP, slug, ext);
|
||||
snprintf(ver_path, sizeof(ver_path), "%s%c%s.version",
|
||||
obs_plugin_dir, PATH_SEP, slug);
|
||||
|
||||
FILE *f = fopen(tmp_path, "wb");
|
||||
if (!f) {
|
||||
dbg_log(LOG_ERROR, "[%s] Cannot open %s for writing",
|
||||
PLUGIN_NAME, tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) {
|
||||
fclose(f);
|
||||
remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
char auth_header[512];
|
||||
snprintf(auth_header, sizeof(auth_header), obf_auth_bearer_fmt(), token);
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
|
||||
char ua[128];
|
||||
snprintf(ua, sizeof(ua), "%s%s", obf_ua_prefix(), PLUGIN_VERSION);
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, f);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, ua);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_set_ssl_opts(curl);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
fclose(f);
|
||||
|
||||
if (res != CURLE_OK || http_code != 200) {
|
||||
dbg_log(LOG_ERROR, "[%s] Download %s failed: %s (HTTP %ld)",
|
||||
PLUGIN_NAME, slug, curl_easy_strerror(res), http_code);
|
||||
remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Atomic replace: delete old, rename tmp */
|
||||
remove(final_path);
|
||||
if (rename(tmp_path, final_path) != 0) {
|
||||
dbg_log(LOG_ERROR, "[%s] Failed to rename %s -> %s",
|
||||
PLUGIN_NAME, tmp_path, final_path);
|
||||
remove(tmp_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Write version file from server response header or plugin list */
|
||||
dbg_log(LOG_INFO, "[%s] Installed %s to %s", PLUGIN_NAME, slug,
|
||||
final_path);
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MAX_PLUGINS 32
|
||||
#define MAX_NAME_LEN 128
|
||||
#define MAX_VER_LEN 32
|
||||
#define MAX_SLUG_LEN 64
|
||||
|
||||
struct plugin_info {
|
||||
char slug[MAX_SLUG_LEN];
|
||||
char name[MAX_NAME_LEN];
|
||||
char latest_version[MAX_VER_LEN];
|
||||
char installed_version[MAX_VER_LEN];
|
||||
bool installed;
|
||||
bool update_available;
|
||||
};
|
||||
|
||||
struct plugin_list {
|
||||
struct plugin_info items[MAX_PLUGINS];
|
||||
int count;
|
||||
};
|
||||
|
||||
bool downloader_fetch_plugin_list(const char *token, struct plugin_list *out);
|
||||
bool downloader_install_plugin(const char *token, const char *slug,
|
||||
const char *obs_plugin_dir);
|
||||
void downloader_detect_installed(struct plugin_list *list,
|
||||
const char *obs_plugin_dir);
|
||||
bool downloader_get_obs_plugin_dir(char *buf, size_t sz);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,98 @@
|
||||
#pragma once
|
||||
|
||||
/*
|
||||
* Minimal dynamic string builder (replaces OBS util/dstr.h).
|
||||
*/
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
|
||||
struct dstr {
|
||||
char *array;
|
||||
size_t len;
|
||||
size_t capacity;
|
||||
};
|
||||
|
||||
static inline void dstr_init(struct dstr *dst)
|
||||
{
|
||||
dst->array = NULL;
|
||||
dst->len = 0;
|
||||
dst->capacity = 0;
|
||||
}
|
||||
|
||||
static inline void dstr_free(struct dstr *dst)
|
||||
{
|
||||
free(dst->array);
|
||||
dst->array = NULL;
|
||||
dst->len = 0;
|
||||
dst->capacity = 0;
|
||||
}
|
||||
|
||||
static inline void dstr_ensure_capacity(struct dstr *dst, size_t new_cap)
|
||||
{
|
||||
if (new_cap <= dst->capacity)
|
||||
return;
|
||||
size_t cap = dst->capacity ? dst->capacity : 64;
|
||||
while (cap < new_cap)
|
||||
cap *= 2;
|
||||
dst->array = (char *)realloc(dst->array, cap);
|
||||
dst->capacity = cap;
|
||||
}
|
||||
|
||||
static inline void dstr_cat(struct dstr *dst, const char *str)
|
||||
{
|
||||
size_t slen = strlen(str);
|
||||
dstr_ensure_capacity(dst, dst->len + slen + 1);
|
||||
memcpy(dst->array + dst->len, str, slen + 1);
|
||||
dst->len += slen;
|
||||
}
|
||||
|
||||
static inline void dstr_ncat(struct dstr *dst, const char *str, size_t len)
|
||||
{
|
||||
dstr_ensure_capacity(dst, dst->len + len + 1);
|
||||
memcpy(dst->array + dst->len, str, len);
|
||||
dst->len += len;
|
||||
dst->array[dst->len] = '\0';
|
||||
}
|
||||
|
||||
static inline void dstr_printf(struct dstr *dst, const char *fmt, ...)
|
||||
{
|
||||
va_list args, args2;
|
||||
va_start(args, fmt);
|
||||
va_copy(args2, args);
|
||||
|
||||
int needed = vsnprintf(NULL, 0, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (needed < 0) {
|
||||
va_end(args2);
|
||||
return;
|
||||
}
|
||||
|
||||
dstr_ensure_capacity(dst, (size_t)needed + 1);
|
||||
vsnprintf(dst->array, (size_t)needed + 1, fmt, args2);
|
||||
dst->len = (size_t)needed;
|
||||
va_end(args2);
|
||||
}
|
||||
|
||||
static inline void dstr_catf(struct dstr *dst, const char *fmt, ...)
|
||||
{
|
||||
va_list args, args2;
|
||||
va_start(args, fmt);
|
||||
va_copy(args2, args);
|
||||
|
||||
int needed = vsnprintf(NULL, 0, fmt, args);
|
||||
va_end(args);
|
||||
|
||||
if (needed < 0) {
|
||||
va_end(args2);
|
||||
return;
|
||||
}
|
||||
|
||||
dstr_ensure_capacity(dst, dst->len + (size_t)needed + 1);
|
||||
vsnprintf(dst->array + dst->len, (size_t)needed + 1, fmt, args2);
|
||||
dst->len += (size_t)needed;
|
||||
va_end(args2);
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
#include <QDialog>
|
||||
#include <QMainWindow>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QFont>
|
||||
#include <QFrame>
|
||||
#include <QMessageBox>
|
||||
#include <QApplication>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
|
||||
#include <obs-frontend-api.h>
|
||||
|
||||
extern "C" {
|
||||
#include "auth.h"
|
||||
#include "downloader.h"
|
||||
}
|
||||
|
||||
#include "manager-dialog.hpp"
|
||||
|
||||
static bool is_de(const char *locale)
|
||||
{
|
||||
return locale && locale[0] == 'd' && locale[1] == 'e';
|
||||
}
|
||||
|
||||
class ManagerDialog : public QDialog {
|
||||
public:
|
||||
ManagerDialog(QWidget *parent, const char *locale)
|
||||
: QDialog(parent), m_locale(locale ? locale : "en")
|
||||
{
|
||||
bool de = is_de(locale);
|
||||
setWindowTitle("stools Plugin Manager");
|
||||
setMinimumSize(600, 450);
|
||||
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(16, 16, 16, 16);
|
||||
mainLayout->setSpacing(12);
|
||||
|
||||
/* ---- Header ---- */
|
||||
auto *header = new QLabel("stools Plugin Manager");
|
||||
QFont headerFont = header->font();
|
||||
headerFont.setPointSize(14);
|
||||
headerFont.setBold(true);
|
||||
header->setFont(headerFont);
|
||||
mainLayout->addWidget(header);
|
||||
|
||||
/* ---- Auth section ---- */
|
||||
m_authFrame = new QFrame();
|
||||
auto *authLayout = new QHBoxLayout(m_authFrame);
|
||||
authLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
m_statusLabel = new QLabel();
|
||||
authLayout->addWidget(m_statusLabel, 1);
|
||||
|
||||
m_tokenInput = new QLineEdit();
|
||||
m_tokenInput->setEchoMode(QLineEdit::Password);
|
||||
m_tokenInput->setPlaceholderText("API Token");
|
||||
m_tokenInput->setMinimumWidth(200);
|
||||
authLayout->addWidget(m_tokenInput);
|
||||
|
||||
m_loginBtn = new QPushButton(de ? "Anmelden" : "Login");
|
||||
connect(m_loginBtn, &QPushButton::clicked, this,
|
||||
&ManagerDialog::onLogin);
|
||||
authLayout->addWidget(m_loginBtn);
|
||||
|
||||
m_logoutBtn = new QPushButton(de ? "Abmelden" : "Logout");
|
||||
connect(m_logoutBtn, &QPushButton::clicked, this,
|
||||
&ManagerDialog::onLogout);
|
||||
authLayout->addWidget(m_logoutBtn);
|
||||
|
||||
mainLayout->addWidget(m_authFrame);
|
||||
|
||||
/* ---- Separator ---- */
|
||||
auto *sep = new QFrame();
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
mainLayout->addWidget(sep);
|
||||
|
||||
/* ---- Plugin table ---- */
|
||||
m_table = new QTableWidget(0, 4);
|
||||
m_table->setHorizontalHeaderLabels(
|
||||
{de ? "Plugin" : "Plugin",
|
||||
de ? "Installiert" : "Installed",
|
||||
de ? "Verfügbar" : "Available", ""});
|
||||
m_table->horizontalHeader()->setStretchLastSection(false);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(
|
||||
0, QHeaderView::Stretch);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(
|
||||
1, QHeaderView::ResizeToContents);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(
|
||||
2, QHeaderView::ResizeToContents);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(
|
||||
3, QHeaderView::ResizeToContents);
|
||||
m_table->verticalHeader()->setVisible(false);
|
||||
m_table->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
mainLayout->addWidget(m_table, 1);
|
||||
|
||||
/* ---- Refresh button ---- */
|
||||
auto *bottomLayout = new QHBoxLayout();
|
||||
m_refreshBtn = new QPushButton(
|
||||
de ? "Aktualisieren" : "Refresh");
|
||||
connect(m_refreshBtn, &QPushButton::clicked, this,
|
||||
&ManagerDialog::onRefresh);
|
||||
bottomLayout->addStretch();
|
||||
bottomLayout->addWidget(m_refreshBtn);
|
||||
mainLayout->addLayout(bottomLayout);
|
||||
|
||||
/* ---- Info label ---- */
|
||||
m_infoLabel = new QLabel(
|
||||
de ? "Änderungen werden nach OBS-Neustart wirksam."
|
||||
: "Changes take effect after restarting OBS.");
|
||||
QFont infoFont = m_infoLabel->font();
|
||||
infoFont.setItalic(true);
|
||||
m_infoLabel->setFont(infoFont);
|
||||
m_infoLabel->setStyleSheet("color: gray;");
|
||||
mainLayout->addWidget(m_infoLabel);
|
||||
|
||||
updateAuthUI();
|
||||
|
||||
if (auth_is_logged_in())
|
||||
onRefresh();
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_locale;
|
||||
QFrame *m_authFrame;
|
||||
QLabel *m_statusLabel;
|
||||
QLineEdit *m_tokenInput;
|
||||
QPushButton *m_loginBtn;
|
||||
QPushButton *m_logoutBtn;
|
||||
QTableWidget *m_table;
|
||||
QPushButton *m_refreshBtn;
|
||||
QLabel *m_infoLabel;
|
||||
struct plugin_list m_plugins = {};
|
||||
|
||||
void updateAuthUI()
|
||||
{
|
||||
bool de = is_de(m_locale.toUtf8().constData());
|
||||
bool logged_in = auth_is_logged_in();
|
||||
|
||||
m_tokenInput->setVisible(!logged_in);
|
||||
m_loginBtn->setVisible(!logged_in);
|
||||
m_logoutBtn->setVisible(logged_in);
|
||||
m_refreshBtn->setEnabled(logged_in);
|
||||
m_table->setEnabled(logged_in);
|
||||
|
||||
if (logged_in) {
|
||||
m_statusLabel->setText(
|
||||
QString(de ? "Angemeldet als: %1"
|
||||
: "Logged in as: %1")
|
||||
.arg(auth_get_username()));
|
||||
m_statusLabel->setStyleSheet("color: green;");
|
||||
} else {
|
||||
m_statusLabel->setText(
|
||||
de ? "Nicht angemeldet"
|
||||
: "Not logged in");
|
||||
m_statusLabel->setStyleSheet("color: red;");
|
||||
}
|
||||
}
|
||||
|
||||
void onLogin()
|
||||
{
|
||||
QString token = m_tokenInput->text().trimmed();
|
||||
if (token.isEmpty()) return;
|
||||
|
||||
m_loginBtn->setEnabled(false);
|
||||
m_loginBtn->setText("...");
|
||||
QApplication::processEvents();
|
||||
|
||||
bool ok = auth_login(token.toUtf8().constData());
|
||||
bool de = is_de(m_locale.toUtf8().constData());
|
||||
|
||||
m_loginBtn->setEnabled(true);
|
||||
m_loginBtn->setText(de ? "Anmelden" : "Login");
|
||||
|
||||
if (ok) {
|
||||
m_tokenInput->clear();
|
||||
updateAuthUI();
|
||||
onRefresh();
|
||||
} else {
|
||||
QMessageBox::warning(this, "stools Plugin Manager",
|
||||
de ? "Ungültiger Token."
|
||||
: "Invalid token.");
|
||||
}
|
||||
}
|
||||
|
||||
void onLogout()
|
||||
{
|
||||
auth_logout();
|
||||
m_table->setRowCount(0);
|
||||
m_plugins.count = 0;
|
||||
updateAuthUI();
|
||||
}
|
||||
|
||||
void onRefresh()
|
||||
{
|
||||
if (!auth_is_logged_in()) return;
|
||||
|
||||
m_refreshBtn->setEnabled(false);
|
||||
m_refreshBtn->setText("...");
|
||||
QApplication::processEvents();
|
||||
|
||||
bool de = is_de(m_locale.toUtf8().constData());
|
||||
|
||||
if (downloader_fetch_plugin_list(auth_get_token(), &m_plugins)) {
|
||||
char obs_dir[512];
|
||||
if (downloader_get_obs_plugin_dir(obs_dir, sizeof(obs_dir)))
|
||||
downloader_detect_installed(&m_plugins, obs_dir);
|
||||
populateTable();
|
||||
} else {
|
||||
QMessageBox::warning(
|
||||
this, "stools Plugin Manager",
|
||||
de ? "Plugin-Liste konnte nicht geladen werden."
|
||||
: "Failed to fetch plugin list.");
|
||||
}
|
||||
|
||||
m_refreshBtn->setEnabled(true);
|
||||
m_refreshBtn->setText(de ? "Aktualisieren" : "Refresh");
|
||||
}
|
||||
|
||||
void populateTable()
|
||||
{
|
||||
bool de = is_de(m_locale.toUtf8().constData());
|
||||
m_table->setRowCount(m_plugins.count);
|
||||
|
||||
for (int i = 0; i < m_plugins.count; i++) {
|
||||
struct plugin_info *pi = &m_plugins.items[i];
|
||||
|
||||
m_table->setItem(i, 0,
|
||||
new QTableWidgetItem(pi->name));
|
||||
|
||||
QString installed_ver =
|
||||
pi->installed
|
||||
? QString(pi->installed_version)
|
||||
: (de ? "Nicht installiert"
|
||||
: "Not installed");
|
||||
m_table->setItem(i, 1,
|
||||
new QTableWidgetItem(installed_ver));
|
||||
|
||||
m_table->setItem(
|
||||
i, 2,
|
||||
new QTableWidgetItem(pi->latest_version));
|
||||
|
||||
QString btn_text;
|
||||
if (!pi->installed)
|
||||
btn_text = de ? "Installieren" : "Install";
|
||||
else if (pi->update_available)
|
||||
btn_text = de ? "Aktualisieren" : "Update";
|
||||
else
|
||||
btn_text = de ? "Aktuell" : "Up to date";
|
||||
|
||||
auto *btn = new QPushButton(btn_text);
|
||||
btn->setEnabled(!pi->installed ||
|
||||
pi->update_available);
|
||||
btn->setProperty("slug",
|
||||
QString(pi->slug));
|
||||
btn->setProperty("version",
|
||||
QString(pi->latest_version));
|
||||
connect(btn, &QPushButton::clicked, this,
|
||||
&ManagerDialog::onInstallClicked);
|
||||
m_table->setCellWidget(i, 3, btn);
|
||||
}
|
||||
}
|
||||
|
||||
void onInstallClicked()
|
||||
{
|
||||
auto *btn = qobject_cast<QPushButton *>(sender());
|
||||
if (!btn) return;
|
||||
|
||||
QString slug = btn->property("slug").toString();
|
||||
QString version = btn->property("version").toString();
|
||||
bool de = is_de(m_locale.toUtf8().constData());
|
||||
|
||||
btn->setEnabled(false);
|
||||
btn->setText("...");
|
||||
QApplication::processEvents();
|
||||
|
||||
char obs_dir[512];
|
||||
if (!downloader_get_obs_plugin_dir(obs_dir, sizeof(obs_dir))) {
|
||||
QMessageBox::critical(
|
||||
this, "stools Plugin Manager",
|
||||
de ? "OBS Plugin-Verzeichnis nicht gefunden."
|
||||
: "OBS plugin directory not found.");
|
||||
btn->setEnabled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok = downloader_install_plugin(
|
||||
auth_get_token(), slug.toUtf8().constData(), obs_dir);
|
||||
|
||||
if (ok) {
|
||||
/* Write version file */
|
||||
char ver_path[512];
|
||||
snprintf(ver_path, sizeof(ver_path), "%s%c%s.version",
|
||||
obs_dir,
|
||||
#ifdef _WIN32
|
||||
'\\',
|
||||
#else
|
||||
'/',
|
||||
#endif
|
||||
slug.toUtf8().constData());
|
||||
FILE *f = fopen(ver_path, "w");
|
||||
if (f) {
|
||||
fputs(version.toUtf8().constData(), f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
btn->setText(de ? "Aktuell" : "Up to date");
|
||||
btn->setEnabled(false);
|
||||
|
||||
QMessageBox::information(
|
||||
this, "stools Plugin Manager",
|
||||
QString(de ? "%1 wurde installiert. Bitte OBS neu starten."
|
||||
: "%1 has been installed. Please restart OBS.")
|
||||
.arg(slug));
|
||||
|
||||
onRefresh();
|
||||
} else {
|
||||
btn->setText(de ? "Fehlgeschlagen" : "Failed");
|
||||
QTimer::singleShot(2000, [btn, de]() {
|
||||
btn->setText(de ? "Erneut versuchen" : "Retry");
|
||||
btn->setEnabled(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void manager_dialog_show(const char *locale)
|
||||
{
|
||||
QMainWindow *main_window =
|
||||
(QMainWindow *)obs_frontend_get_main_window();
|
||||
auto *dialog = new ManagerDialog(main_window, locale);
|
||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||
dialog->show();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void manager_dialog_show(const char *locale);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "obfuscation.h"
|
||||
#include <string>
|
||||
|
||||
#define OBF_FUNC(name, literal) \
|
||||
static const std::string &name##_storage() \
|
||||
{ \
|
||||
static const std::string s{literal}; \
|
||||
return s; \
|
||||
} \
|
||||
extern "C" const char *name(void) \
|
||||
{ \
|
||||
return name##_storage().c_str(); \
|
||||
}
|
||||
|
||||
OBF_FUNC(obf_https_prefix, "https://")
|
||||
OBF_FUNC(obf_stools_host, "stools.cc")
|
||||
OBF_FUNC(obf_auth_bearer_fmt, "Authorization: Bearer %s")
|
||||
OBF_FUNC(obf_ua_prefix, "stools-pluginmanager/")
|
||||
OBF_FUNC(obf_api_me_path, "/api/me")
|
||||
OBF_FUNC(obf_api_plugins_path, "/api/plugins")
|
||||
OBF_FUNC(obf_api_plugin_download_fmt, "/api/plugins/%s/download?platform=%s")
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
const char *obf_https_prefix(void);
|
||||
const char *obf_stools_host(void);
|
||||
const char *obf_auth_bearer_fmt(void);
|
||||
const char *obf_ua_prefix(void);
|
||||
const char *obf_api_me_path(void);
|
||||
const char *obf_api_plugins_path(void);
|
||||
const char *obf_api_plugin_download_fmt(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,126 @@
|
||||
#include <obs-module.h>
|
||||
#include <obs-frontend-api.h>
|
||||
#include <pthread.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "compat.h"
|
||||
#include "debug-log.h"
|
||||
#include "auth.h"
|
||||
#include "downloader.h"
|
||||
#include "manager-dialog.hpp"
|
||||
|
||||
OBS_DECLARE_MODULE()
|
||||
|
||||
#define PLUGIN_NAME "stools Plugin Manager"
|
||||
|
||||
static const char *g_locale = NULL;
|
||||
|
||||
/* ---- Background init thread ---- */
|
||||
|
||||
static pthread_t g_init_thread;
|
||||
static volatile bool g_init_done = false;
|
||||
|
||||
static void *init_thread_func(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
|
||||
os_set_thread_name("stools-pm-init");
|
||||
|
||||
auth_init();
|
||||
|
||||
if (auth_is_logged_in()) {
|
||||
struct plugin_list plugins = {0};
|
||||
if (downloader_fetch_plugin_list(auth_get_token(), &plugins)) {
|
||||
char obs_dir[512];
|
||||
if (downloader_get_obs_plugin_dir(obs_dir,
|
||||
sizeof(obs_dir))) {
|
||||
downloader_detect_installed(&plugins, obs_dir);
|
||||
|
||||
for (int i = 0; i < plugins.count; i++) {
|
||||
struct plugin_info *pi =
|
||||
&plugins.items[i];
|
||||
if (!pi->installed ||
|
||||
pi->update_available) {
|
||||
dbg_log(LOG_INFO,
|
||||
"[%s] Auto-installing %s v%s",
|
||||
PLUGIN_NAME, pi->slug,
|
||||
pi->latest_version);
|
||||
if (downloader_install_plugin(
|
||||
auth_get_token(),
|
||||
pi->slug,
|
||||
obs_dir)) {
|
||||
char vp[512];
|
||||
snprintf(vp,
|
||||
sizeof(vp),
|
||||
"%s"
|
||||
#ifdef _WIN32
|
||||
"\\"
|
||||
#else
|
||||
"/"
|
||||
#endif
|
||||
"%s.version",
|
||||
obs_dir,
|
||||
pi->slug);
|
||||
FILE *f = fopen(vp,
|
||||
"w");
|
||||
if (f) {
|
||||
fputs(pi->latest_version,
|
||||
f);
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g_init_done = true;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- Tools menu ---- */
|
||||
|
||||
static void tools_menu_cb(void *private_data)
|
||||
{
|
||||
(void)private_data;
|
||||
manager_dialog_show(g_locale);
|
||||
}
|
||||
|
||||
/* ---- Module lifecycle ---- */
|
||||
|
||||
bool obs_module_load(void)
|
||||
{
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
|
||||
g_locale = obs_get_locale();
|
||||
|
||||
if (pthread_create(&g_init_thread, NULL, init_thread_func, NULL) != 0) {
|
||||
dbg_log(LOG_ERROR, "[%s] Failed to create init thread",
|
||||
PLUGIN_NAME);
|
||||
}
|
||||
|
||||
dbg_log(LOG_INFO, "[%s] Plugin loaded (v%s)", PLUGIN_NAME,
|
||||
PLUGIN_VERSION);
|
||||
return true;
|
||||
}
|
||||
|
||||
void obs_module_post_load(void)
|
||||
{
|
||||
bool de = g_locale && g_locale[0] == 'd' && g_locale[1] == 'e';
|
||||
obs_frontend_add_tools_menu_item(
|
||||
de ? "stools Plugin Manager" : "stools Plugin Manager",
|
||||
tools_menu_cb, NULL);
|
||||
}
|
||||
|
||||
void obs_module_unload(void)
|
||||
{
|
||||
if (!g_init_done) {
|
||||
pthread_join(g_init_thread, NULL);
|
||||
}
|
||||
auth_shutdown();
|
||||
curl_global_cleanup();
|
||||
dbg_log(LOG_INFO, "[%s] Plugin unloaded", PLUGIN_NAME);
|
||||
}
|
||||
Reference in New Issue
Block a user