Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Go to the [Releases](https://github.com/JavBoss/pornboss/releases) page, downloa
### 2. Start the App

- Windows: double-click `pornboss.exe`. If SmartScreen blocks it on first launch, click "More info" and continue.
- macOS: right-click `pornboss.command` and choose Open. If macOS shows a security warning, continue anyway.
- macOS: open `Pornboss.app` in the extracted folder. If Gatekeeper blocks it on first launch, right-click the app, choose Open, and continue anyway.
- Linux: run `pornboss`

After launch, Pornboss will try to open your browser automatically. If it does not, open the local address shown in the terminal manually.
Expand Down
2 changes: 1 addition & 1 deletion README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ porn manager, jav manager, av manager, jav scraper, jav metadata, adult video ma
### 2. 启动程序

- Windows:双击 `pornboss.exe`;首次运行可能会被smartScreen阻止,点击更多信息->仍要运行
- macOS:右键 `pornboss.command` 点击打开;如果系统弹出安全警告,仍然选择继续打开
- macOS:打开解压目录里的 `Pornboss.app`;如果首次启动被系统拦截,右键应用后选择“打开”,再继续运行
- Linux:运行 `pornboss`

启动成功后,程序会自动尝试打开浏览器;如果没有自动打开,可以手动访问终端里显示的本地地址。
Expand Down
10 changes: 8 additions & 2 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"pornboss/internal/manager"

"github.com/gin-gonic/gin"
"github.com/mattn/go-isatty"
"gopkg.in/natefinch/lumberjack.v2"
)

Expand Down Expand Up @@ -148,8 +149,10 @@ func main() {
actualPort := listener.Addr().(*net.TCPAddr).Port
url := fmt.Sprintf("http://localhost:%d", actualPort)
fmt.Printf("Pornboss启动成功,浏览器访问地址:%s\n", url)
if err := util.OpenFile(url); err != nil {
logger.Printf("open browser failed: %v", err)
if os.Getenv("PORNBOSS_LAUNCHER") == "" {
if err := util.OpenFile(url); err != nil {
logger.Printf("open browser failed: %v", err)
}
}
logger.Printf("server listening on %s", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
Expand Down Expand Up @@ -246,6 +249,9 @@ func resolveStaticDir(staticDir string) string {
}

func waitForUserExit() {
if os.Getenv("PORNBOSS_LAUNCHER") != "" || !isatty.IsTerminal(os.Stdin.Fd()) {
return
}
fmt.Println("请手动关闭此窗口,或按回车键退出。")
reader := bufio.NewReader(os.Stdin)
if _, err := reader.ReadString('\n'); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion scripts/cli.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ NEED_BUILD=0
if [[ ! -f "$CLI_BIN" ]]; then
NEED_BUILD=1
else
if find "$CLI_ROOT" -type f \( -name "*.mjs" -o -name "*.js" -o -name "*.json" \) \
if find "$CLI_ROOT" -type f \( -name "*.mjs" -o -name "*.js" -o -name "*.json" -o -name "*.swift" \) \
! -path "$CLI_ROOT/node_modules/*" ! -path "$CLI_ROOT/build/*" \
-newer "$CLI_BIN" -print -quit | grep -q .; then
NEED_BUILD=1
Expand Down
174 changes: 139 additions & 35 deletions scripts/cli/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const ROOT_DIR = findRepoRoot(entryDirFromArgv());
const WEB_DIR = path.join(ROOT_DIR, "web");
const INTERNAL_BIN_DIR = path.join(ROOT_DIR, "internal", "bin");
const BIN_DIR = path.join(ROOT_DIR, "bin");
const MACOS_LAUNCHER_SOURCE = path.join(ROOT_DIR, "scripts", "cli", "macos-launcher.swift");

const PLATFORM_CHOICES = [
{ label: "windows-x86_64", goos: "windows", goarch: "amd64" },
Expand Down Expand Up @@ -162,6 +163,29 @@ function runCommand(cmd, args, options = {}) {
});
}

function runCommandCapture(cmd, args, options = {}) {
return new Promise((resolve, reject) => {
const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"], ...options });
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve(stdout);
} else {
const detail = stderr.trim() || stdout.trim();
reject(new Error(detail || `${cmd} exited with code ${code}`));
}
});
});
}

function commandExists(cmd) {
return new Promise((resolve) => {
const probe = process.platform === "win32" ? "where" : "which";
Expand Down Expand Up @@ -275,7 +299,12 @@ async function buildBackendRelease(choice, outDir) {
}
}

const binName = choice.goos === "windows" ? "pornboss.exe" : "pornboss";
const binName =
choice.goos === "windows"
? "pornboss.exe"
: choice.goos === "darwin"
? "pornboss-server"
: "pornboss";
const binPath = path.join(outDir, binName);
const env = {
...process.env,
Expand Down Expand Up @@ -314,34 +343,106 @@ async function copyBundledFfmpeg(choice, outDir) {
}
}

async function createMacCommandLauncher(outDir) {
const launcherPath = path.join(outDir, "pornboss.command");
const launcherContent = [
"#!/bin/bash",
"set -u",
'QUARANTINE_ATTR="com.apple.quarantine"',
'SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"',
'cd "$SCRIPT_DIR" || exit 1',
"",
'if command -v xattr >/dev/null 2>&1; then',
' xattr -dr "$QUARANTINE_ATTR" "$SCRIPT_DIR" >/dev/null 2>&1 || true',
"fi",
"",
'"$SCRIPT_DIR/pornboss" "$@"',
"status=$?",
'if [ "$status" -ne 0 ]; then',
' echo',
' echo "Pornboss exited with status $status."',
' read -r -p "Press Enter to close..." _',
"fi",
'exit "$status"',
"",
].join("\n");

await fsp.writeFile(launcherPath, launcherContent);
function releaseOutDir(choice, version) {
return path.join(ROOT_DIR, "release", `pornboss-${version}-${choice.label}`);
}

function releaseAppDir(choice, outDir) {
if (choice.goos === "darwin") {
return path.join(outDir, "Pornboss.app");
}
return outDir;
}

function releasePayloadDir(choice, outDir) {
if (choice.goos === "darwin") {
return path.join(releaseAppDir(choice, outDir), "Contents", "MacOS");
}
return outDir;
}

function macLauncherTarget(choice) {
if (choice.goarch === "arm64") {
return { triple: "arm64-apple-macos11.0", minimumVersion: "11.0" };
}
return { triple: "x86_64-apple-macos10.13", minimumVersion: "10.13" };
}

async function compileMacLauncher(choice, appDir) {
if (process.platform !== "darwin") {
throw new Error("macOS .app 打包需要在 macOS 主机上运行 release");
}
if (!(await exists(MACOS_LAUNCHER_SOURCE))) {
throw new Error(`缺少 macOS launcher 源文件:${MACOS_LAUNCHER_SOURCE}`);
}
if (!(await commandExists("swiftc"))) {
throw new Error("缺少 swiftc,请先安装 Xcode 或 Command Line Tools");
}
if (!(await commandExists("xcrun"))) {
throw new Error("缺少 xcrun,请先安装 Xcode 或 Command Line Tools");
}

const sdkPath = (await runCommandCapture("xcrun", ["--sdk", "macosx", "--show-sdk-path"])).trim();
const target = macLauncherTarget(choice);
const launcherPath = path.join(appDir, "Contents", "MacOS", "Pornboss");
await runCommand(
"swiftc",
[
"-O",
"-target",
target.triple,
"-sdk",
sdkPath,
MACOS_LAUNCHER_SOURCE,
"-o",
launcherPath,
],
{ cwd: ROOT_DIR },
);
await fsp.chmod(launcherPath, 0o755);
}

async function createMacAppBundle(choice, appDir, version) {
const contentsDir = path.join(appDir, "Contents");
const macosDir = path.join(contentsDir, "MacOS");
const resourcesDir = path.join(contentsDir, "Resources");
await fsp.mkdir(macosDir, { recursive: true });
await fsp.mkdir(resourcesDir, { recursive: true });

const infoPlistPath = path.join(contentsDir, "Info.plist");
const target = macLauncherTarget(choice);
const infoPlist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Pornboss</string>
<key>CFBundleExecutable</key>
<string>Pornboss</string>
<key>CFBundleIdentifier</key>
<string>com.javboss.pornboss</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Pornboss</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>${version}</string>
<key>CFBundleVersion</key>
<string>${version}</string>
<key>LSMinimumSystemVersion</key>
<string>${target.minimumVersion}</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
`;
await fsp.writeFile(infoPlistPath, infoPlist);
}

async function createZip(outDir, zipPath) {
const hasZip = await commandExists("zip");
if (!hasZip) {
Expand All @@ -363,20 +464,23 @@ async function runRelease(choice, version) {
return;
}

const outDir = path.join(ROOT_DIR, "release", `pornboss-${version}-${choice.label}`);
const outDir = releaseOutDir(choice, version);
const appDir = releaseAppDir(choice, outDir);
const payloadDir = releasePayloadDir(choice, outDir);
await fsp.rm(outDir, { recursive: true, force: true });
await fsp.mkdir(outDir, { recursive: true });
await fsp.mkdir(payloadDir, { recursive: true });
if (choice.goos === "darwin") {
await createMacAppBundle(choice, appDir, version);
console.log("[release] 编译 macOS 原生 launcher");
await compileMacLauncher(choice, appDir);
}

await buildWeb();
console.log("[release] 复制前端资源");
await copyDir(path.join(WEB_DIR, "dist"), path.join(outDir, "web", "dist"));
await buildBackendRelease(choice, outDir);
await copyDir(path.join(WEB_DIR, "dist"), path.join(payloadDir, "web", "dist"));
await buildBackendRelease(choice, payloadDir);
console.log("[release] 复制 ffmpeg/ffprobe");
await copyBundledFfmpeg(choice, outDir);
if (choice.goos === "darwin") {
console.log("[release] 生成 macOS .command 启动器");
await createMacCommandLauncher(outDir);
}
await copyBundledFfmpeg(choice, payloadDir);

const zipPath = path.join(
ROOT_DIR,
Expand Down
Loading