Skip to content

Commit 8543663

Browse files
authored
Merge pull request #2 from HarryXin0919/add-one-click-launcher
Add one click launcher
2 parents 5667d9e + 3c4cf43 commit 8543663

27 files changed

Lines changed: 968 additions & 45 deletions

.github/workflows/release.yml

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
name: Release
2+
3+
# 打一个 v* 版本标签(例:git tag v0.2.0 && git push --tags)就会:
4+
# 在 Windows / macOS / Linux 各构建一份桌面 app,压缩,发布到一个 GitHub Release。
5+
# 也可在 Actions 页手动触发(workflow_dispatch)只构建、不发布。
6+
on:
7+
push:
8+
tags: ["v*"]
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: write # 发布 Release 需要写权限
13+
14+
jobs:
15+
build:
16+
name: Build (${{ matrix.os }})
17+
runs-on: ${{ matrix.os }}
18+
strategy:
19+
fail-fast: false
20+
matrix:
21+
include:
22+
- os: windows-latest
23+
asset: viralens-windows-x64.zip
24+
- os: macos-latest
25+
asset: viralens-macos-arm64.zip
26+
- os: ubuntu-latest
27+
asset: viralens-linux-x64.zip
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- uses: actions/setup-python@v5
32+
with:
33+
python-version: "3.12" # 各重依赖在 3.12 上 wheel 最全、最稳
34+
35+
- name: Install deps + PyInstaller
36+
run: |
37+
python -m pip install --upgrade pip
38+
python -m pip install -e .
39+
python -m pip install pyinstaller
40+
41+
# 原生窗口后端只在 Windows/macOS 装(用系统自带 webview)。Linux 的 GTK webview
42+
# 打包困难,故不装 —— Linux 包会自动回退到「开浏览器」(见 viralens_app.py)。
43+
- name: Install native-window backend (Windows/macOS only)
44+
if: runner.os != 'Linux'
45+
run: python -m pip install pywebview
46+
47+
- name: Build (PyInstaller, onedir)
48+
run: python -m PyInstaller --noconfirm --clean packaging/viralens.spec
49+
50+
- name: Package (Windows)
51+
if: runner.os == 'Windows'
52+
shell: pwsh
53+
run: Compress-Archive -Path dist/viralens -DestinationPath ${{ matrix.asset }}
54+
55+
- name: Package (macOS) # ditto 是 mac 上正确打包 .app 的方式(保留符号链接/权限)
56+
if: runner.os == 'macOS'
57+
run: ditto -c -k --keepParent dist/viralens.app ${{ matrix.asset }}
58+
59+
- name: Package (Linux) # zip 保留可执行位,解压后 ./viralens/viralens 直接能跑
60+
if: runner.os == 'Linux'
61+
run: (cd dist && zip -ry ../${{ matrix.asset }} viralens)
62+
63+
- uses: actions/upload-artifact@v4
64+
with:
65+
name: ${{ matrix.asset }}
66+
path: ${{ matrix.asset }}
67+
if-no-files-found: error
68+
69+
release:
70+
name: Publish Release
71+
needs: build
72+
if: startsWith(github.ref, 'refs/tags/')
73+
runs-on: ubuntu-latest
74+
steps:
75+
- uses: actions/download-artifact@v4
76+
with:
77+
path: artifacts
78+
merge-multiple: true
79+
80+
- name: Create GitHub Release
81+
uses: softprops/action-gh-release@v2
82+
with:
83+
files: artifacts/*.zip
84+
generate_release_notes: true
85+
body: |
86+
## viralens 桌面版下载
87+
88+
按你的系统选一个,**下载 → 解压 → 双击即用**,无需自己装 Python。
89+
90+
| 系统 | 文件 | 怎么打开 |
91+
|---|---|---|
92+
| Windows | `viralens-windows-x64.zip` | 解压后运行文件夹里的 `viralens.exe` |
93+
| macOS (Apple Silicon) | `viralens-macos-arm64.zip` | 解压得 `viralens.app`,见下方 macOS 首次打开说明 |
94+
| Linux (x64) | `viralens-linux-x64.zip` | 解压后运行 `./viralens/viralens` |
95+
96+
> **首次打开提示是正常的 —— 因为这个 app 没有花钱做苹果/微软的代码签名,不是有问题。**
97+
> - **Windows**:若弹出「Windows 已保护你的电脑」,点 **更多信息 → 仍要运行**。
98+
> - **macOS**:先试 **右键 → 打开**;若提示「已损坏 / 无法验证开发者」(macOS 15 Sequoia 起常见),改去 **系统设置 → 隐私与安全性**,在底部点「**仍要打开**」;或在终端执行 `xattr -dr com.apple.quarantine /把/viralens.app/拖进来` 后再双击。
99+
100+
启动后会直接弹出 **viralens 应用窗口**(没有终端、没有浏览器标签页;没装系统 WebView 的部分 Linux 环境会自动回退到用浏览器打开)。首次在界面里填入你的 **B站 SESSDATA** 和/或 **YouTube API key** 即可开始抓取分析。
101+
102+
> 可选功能「下视频分析开场镜头 + 配乐」需要自行安装 **ffmpeg**(其余抓取 / 分析 / 报告功能都无需它)。

README.md

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
<img src="banner.svg" alt="viralens" width="100%">
44

55
[![License: MIT](https://img.shields.io/badge/License-MIT-FD4E63.svg)](LICENSE)
6+
&nbsp;[![Download](https://img.shields.io/badge/⬇%20Download-Win%20·%20Mac%20·%20Linux-2EA44F)](https://github.com/HarryXin0919/viralens/releases/latest)
67
&nbsp;![Python](https://img.shields.io/badge/Python-3.10+-FD4E63)
78
&nbsp;![Platforms](https://img.shields.io/badge/Bilibili%20+%20YouTube-FD4E63)
89
&nbsp;![Approach](https://img.shields.io/badge/hypothesis--driven-✓-FD4E63)
@@ -133,6 +134,28 @@ because `play_per_day` inflates new uploads) caught two real declines: **a lifes
133134

134135
## Run it on *your* creators
135136

137+
### Easiest: download the app — no Python, no install
138+
139+
Grab the build for your OS from the **[latest release](https://github.com/HarryXin0919/viralens/releases/latest)**,
140+
unzip, and double-click. It opens in **its own app window** — no terminal, no browser tab; nothing leaves your machine.
141+
(On a Linux box without a system WebView, it falls back to opening the UI in your browser.)
142+
143+
| OS | File | Open it |
144+
|---|---|---|
145+
| **Windows** | `viralens-windows-x64.zip` | unzip → run `viralens\viralens.exe` |
146+
| **macOS** (Apple Silicon) | `viralens-macos-arm64.zip` | unzip → `viralens.app` (see first-launch note below) |
147+
| **Linux** (x64) | `viralens-linux-x64.zip` | unzip → run `./viralens/viralens` |
148+
149+
> **The first-launch security prompt is expected** — the app isn't code-signed (that needs paid Apple/Microsoft certificates), it isn't broken.
150+
> - **Windows:** if you see *"Windows protected your PC"*, click **More info → Run anyway**.
151+
> - **macOS:** try **right-click → Open** first; if it says *"damaged / can't verify developer"* (common since macOS 15 Sequoia), go to **System Settings → Privacy & Security** and click **Open Anyway** at the bottom — or run `xattr -dr com.apple.quarantine /path/to/viralens.app` in Terminal, then double-click.
152+
153+
On first launch, paste your **Bilibili SESSDATA** and/or free **YouTube API key** right in the UI.
154+
Your keys and data are stored in your user folder (`%LOCALAPPDATA%\viralens` · `~/Library/Application Support/viralens` · `~/.local/share/viralens`), never inside the app or in git.
155+
The optional *opening-shots + BGM* analysis needs [ffmpeg](https://ffmpeg.org) installed; everything else works without it.
156+
157+
### From source (for developers)
158+
136159
```bash
137160
git clone https://github.com/HarryXin0919/viralens.git
138161
cd viralens
@@ -248,6 +271,7 @@ rate-limit · small *n* is reported as *"weak signal,"* never dressed up as proo
248271
- [x] Cross-language extension to English YouTube (Entertainment-YT, 4 creators) — done; held without a counter-case
249272
- [x] One-command front door — `python viralens.py` (just the data → CSV/JSON) · `--report` (data + full analysis + report)
250273
- [x] Self-contained interactive HTML report — `reports/index.html`
274+
- [x] Downloadable desktop app for Windows / macOS / Linux — no Python install needed ([releases](https://github.com/HarryXin0919/viralens/releases/latest))
251275
- [ ] Per-creator (not keyword-based) signature-form definition
252276
- [ ] Opt-in LLM layer for qualitative "why this form works" summaries
253277

@@ -288,8 +312,13 @@ viralens 是一个**取数 + 分析**的开源小工具。在一个配置文件
288312
附带还能做:**分区基准**(把你放进同区"典型创作者"里定位)和**疲态检测**(只用满 30 天的成熟
289313
视频总播放判断你在涨还是在跌,已抓到生活区、美食区各一例真实下滑)。
290314

291-
跑法见上方 **Run it on your creators**:改 `scripts/creators.py` 填你想看的任意 B站 / YouTube 创作者,
292-
然后 `python scripts/viralens.py`(只要数据)或 `python scripts/viralens.py --report`(数据 + 分析)。
315+
**怎么用 —— 最省事:下载桌面 app。****[Releases 页](https://github.com/HarryXin0919/viralens/releases/latest)**
316+
按系统下载(Windows / macOS / Linux),解压双击即用,**无需自己装 Python**;启动后浏览器自动打开界面,
317+
在里面填入你的 **B站 SESSDATA** 和/或 **YouTube API key** 即可。你的密钥与数据存在本机用户目录,绝不进 app 包、也不进 git。
318+
319+
想从源码跑(开发者):见上方 **Run it on your creators** —— 改 `scripts/creators.py` 填你想看的任意 B站 / YouTube 创作者,
320+
然后 `python scripts/viralens.py`(只要数据)或 `python scripts/viralens.py --report`(数据 + 分析);
321+
也可 `python scripts/app.py` 开本地网页界面。
293322

294323
---
295324

packaging/build-local.ps1

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# viralens · 本地构建 Windows 桌面 app(给开发者验证用;正式三平台包由 GitHub Actions 出)
2+
#
3+
# 用法: powershell -ExecutionPolicy Bypass -File packaging\build-local.ps1
4+
# 产物: dist\viralens\viralens.exe (整个 dist\viralens\ 文件夹即可压缩分发)
5+
#
6+
# 需要本机已装 Python 3.10+(3.12 最稳)。脚本会装好打包所需依赖,再跑 PyInstaller。
7+
$ErrorActionPreference = "Stop"
8+
Set-Location (Split-Path $PSScriptRoot -Parent) # 切到仓库根
9+
10+
# 找 Python:优先 py -3,退而求其次 python
11+
$py = $null
12+
if (Get-Command py -ErrorAction SilentlyContinue) { $py = "py"; $pyArgs = @("-3") }
13+
elseif (Get-Command python -ErrorAction SilentlyContinue) { $py = "python"; $pyArgs = @() }
14+
else { Write-Error "没找到 Python。先装 Python 3.10+ 并勾选 Add to PATH。"; exit 1 }
15+
16+
Write-Host "[*] 用解释器:" -NoNewline; & $py @pyArgs --version
17+
18+
Write-Host "[*] 安装运行依赖(来自 pyproject)+ 原生窗口后端 + PyInstaller ..."
19+
& $py @pyArgs -m pip install --upgrade pip
20+
& $py @pyArgs -m pip install -e ".[gui]" # [gui] = pywebview,双击弹原生窗口
21+
& $py @pyArgs -m pip install pyinstaller
22+
23+
Write-Host "[*] 打包(onedir)..."
24+
& $py @pyArgs -m PyInstaller --noconfirm --clean packaging/viralens.spec
25+
26+
$exe = Join-Path (Get-Location) "dist\viralens\viralens.exe"
27+
if (Test-Path $exe) {
28+
Write-Host "[OK] 构建完成 -> $exe"
29+
Write-Host " 双击 viralens.exe 即可启动;整个 dist\viralens\ 文件夹打包(zip)就能分发。"
30+
} else {
31+
Write-Error "构建结束但没找到 $exe —— 看上面的 PyInstaller 日志。"
32+
exit 1
33+
}

packaging/viralens.spec

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# -*- mode: python ; coding: utf-8 -*-
2+
"""
3+
PyInstaller 打包配方:把 viralens 连同一个真正的 Python 解释器冻进一个文件夹,
4+
用户无需自己装 Python / pip 依赖,双击即用。三平台(Win/Mac/Linux)同一份 spec。
5+
6+
本地构建(Windows): py -m PyInstaller --noconfirm --clean packaging/viralens.spec
7+
产物: dist/viralens/viralens(.exe) ← onedir,整个文件夹打包分发
8+
9+
为什么用 onedir 而不是 onefile:流水线一次运行会让 app 自己重新拉起 ~6 次
10+
(fetch → 各分析步骤)。onefile 每次启动都要把上百 MB 解压到临时目录,会非常慢;
11+
onedir 直接就地运行,子步骤秒起。
12+
"""
13+
import os
14+
import sys
15+
from PyInstaller.utils.hooks import collect_all
16+
17+
REPO = os.path.dirname(SPECPATH) # SPECPATH 由 PyInstaller 注入 = packaging/
18+
SCRIPTS = os.path.join(REPO, "scripts")
19+
ENTRY = os.path.join(SPECPATH, "viralens_app.py")
20+
ICON = os.path.join(SPECPATH, "icon.ico") # 可选;不存在就不用
21+
22+
datas, binaries, hiddenimports = [], [], []
23+
24+
# —— 重依赖:连子模块 + 数据文件(jieba 词典、bilibili_api 资源等)一起收 ——
25+
for pkg in ("bilibili_api", "aiohttp", "jieba"):
26+
d, b, h = collect_all(pkg)
27+
datas += d
28+
binaries += b
29+
hiddenimports += h
30+
31+
# matplotlib / numpy / Pillow 自带 PyInstaller hook,会自动带数据;这里补 Agg 后端保险
32+
hiddenimports += ["matplotlib.backends.backend_agg", "numpy", "PIL"]
33+
34+
# 原生窗口后端(可选):装了 pywebview 就连同其平台后端一起收进来,实现「双击弹原生窗口」;
35+
# 没装(比如 Linux 不打包 webview)则打成「开浏览器」回退版 —— 入口 viralens_app.py 会自动判断。
36+
try:
37+
import webview # noqa: F401
38+
_d, _b, _h = collect_all("webview")
39+
datas += _d
40+
binaries += _b
41+
hiddenimports += _h
42+
except Exception:
43+
pass
44+
45+
# —— 项目自己的脚本 ——
46+
# app 懒加载它们、viralens 通过 `--vl-exec <模块名>` 用 runpy 跑它们,
47+
# 静态分析有可能看不全,这里全部显式声明,确保都被冻进去。
48+
hiddenimports += [
49+
"runtime", "app", "viralens",
50+
"fetch_multi", "fetch_bilibili", "fetch_youtube",
51+
"compare_form", "creator_profile", "scan_signals", "charts", "export_data", "build_report",
52+
"diagnose", "analyze_video", "import_private",
53+
"creators", "features", "benchmarks",
54+
"classify_and_stats", "comments", "compare_meme", "fetch_covers",
55+
"fetch_videos", "resolve_creators", "subtitle",
56+
]
57+
58+
# —— 只读资源:网页界面 + 配置模板 —— 放进打包根目录,app.py 用 runtime.ASSET_DIR 找它们 ——
59+
datas += [
60+
(os.path.join(SCRIPTS, "gui.html"), "."),
61+
(os.path.join(SCRIPTS, "diagnose.html"), "."),
62+
(os.path.join(SCRIPTS, "config_local.example.py"), "."),
63+
]
64+
65+
a = Analysis(
66+
[ENTRY],
67+
pathex=[SCRIPTS], # 让 import runtime / app / 各脚本 找得到
68+
binaries=binaries,
69+
datas=datas,
70+
hiddenimports=hiddenimports,
71+
hookspath=[],
72+
runtime_hooks=[],
73+
# yt_dlp 体积巨大且只服务于「下 YouTube 视频开头」这个可选功能(缺了会优雅降级);
74+
# tkinter 是 GUI 工具包,我们用 matplotlib 的 Agg 后端、用不到它。
75+
# 注意:不要排 unittest/test —— matplotlib→pyparsing.testing 会在导入时 import unittest。
76+
excludes=["yt_dlp", "tkinter"],
77+
noarchive=False,
78+
)
79+
80+
pyz = PYZ(a.pure)
81+
82+
exe = EXE(
83+
pyz,
84+
a.scripts,
85+
[],
86+
exclude_binaries=True,
87+
name="viralens",
88+
debug=False,
89+
bootloader_ignore_signals=False,
90+
strip=False,
91+
upx=False,
92+
console=False, # 窗口模式:不弹终端。子进程 stdout 由 viralens_app._ensure_std() 接回管道
93+
icon=(ICON if os.path.exists(ICON) else None),
94+
)
95+
96+
coll = COLLECT(
97+
exe,
98+
a.binaries,
99+
a.datas,
100+
strip=False,
101+
upx=False,
102+
name="viralens",
103+
)
104+
105+
# macOS:在 onedir 外再包一层 .app,双击即用(无终端窗口)。
106+
if sys.platform == "darwin":
107+
app_bundle = BUNDLE(
108+
coll,
109+
name="viralens.app",
110+
icon=(ICON if os.path.exists(ICON) else None),
111+
bundle_identifier="dev.harryxin.viralens",
112+
info_plist={
113+
"CFBundleName": "viralens",
114+
"CFBundleDisplayName": "viralens",
115+
"NSHighResolutionCapable": True,
116+
"LSBackgroundOnly": False,
117+
},
118+
)

packaging/viralens_app.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
viralens · 打包成桌面 app 时的入口(被 PyInstaller 冻结,见 viralens.spec)。
3+
4+
目标:双击直接弹出一个**原生应用窗口**(pywebview)——没有终端、没有浏览器标签页。
5+
缺原生 webview 运行时(如部分 Linux 没装 WebKitGTK)时,自动回退到「开浏览器」。
6+
7+
两种角色,靠命令行第一个参数区分:
8+
· 正常双击启动 → 起本地服务器 + 开原生窗口(或回退浏览器)。
9+
· 自己重新拉起 → 形如 `viralens --vl-exec fetch_multi --force`:流水线某一步,
10+
由 runtime.dispatch_if_worker() 接管、当成 __main__ 跑掉再退出(不开窗口)。
11+
12+
注意:本 app 以「窗口模式」(console=False)打包,没有控制台。下面 _ensure_std() 负责:
13+
· 子步骤进程(被父进程用管道收 stdout 显示进度)→ 把标准输出接回那条管道;
14+
· 主窗口进程(双击,没有任何 std 句柄)→ 接到 devnull,避免 print 崩溃。
15+
"""
16+
import io
17+
import os
18+
import sys
19+
20+
21+
def _ensure_std():
22+
"""窗口模式下 sys.stdout/stderr 可能是 None。子进程的句柄是父进程给的管道(有效)→
23+
接回去让进度能被收集;主窗口进程没有有效句柄 → dup 失败,退到 devnull。"""
24+
for name, fd in (("stdout", 1), ("stderr", 2)):
25+
if getattr(sys, name, None) is None:
26+
try:
27+
stream = io.TextIOWrapper(os.fdopen(os.dup(fd), "wb"),
28+
encoding="utf-8", errors="replace", line_buffering=True)
29+
except Exception:
30+
stream = open(os.devnull, "w", encoding="utf-8", errors="replace")
31+
setattr(sys, name, stream)
32+
33+
34+
_ensure_std()
35+
36+
import runtime
37+
38+
runtime.bootstrap()
39+
runtime.dispatch_if_worker() # 若是 --vl-exec 子步骤:跑完即退出,绝不往下走(不开窗口)
40+
41+
import app
42+
43+
# —— 主进程:起服务器,开原生窗口;开不出来就回退浏览器 ——
44+
srv, url = app.start_server()
45+
46+
_opened = False
47+
try:
48+
import webview
49+
webview.create_window("viralens", url, width=1180, height=820, min_size=(900, 600))
50+
webview.start() # 阻塞,直到用户关掉窗口
51+
_opened = True
52+
except Exception:
53+
_opened = False
54+
55+
if not _opened:
56+
# 没有可用的原生 webview(如 Linux 缺 WebKitGTK)→ 开浏览器并挂住进程
57+
import threading
58+
import webbrowser
59+
try:
60+
webbrowser.open(url)
61+
except Exception:
62+
pass
63+
try:
64+
threading.Event().wait()
65+
except KeyboardInterrupt:
66+
pass

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ dependencies = [
2626
"numpy>=1.23.0",
2727
]
2828

29+
# 仅在「打包成桌面 app」时需要:原生窗口后端。源码 CLI / 网页界面都不依赖它。
30+
[project.optional-dependencies]
31+
gui = ["pywebview>=5.0"]
32+
2933
[project.urls]
3034
Homepage = "https://github.com/HarryXin0919/viralens"
3135
Repository = "https://github.com/HarryXin0919/viralens"

0 commit comments

Comments
 (0)