Skip to content
Merged
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
102 changes: 102 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Release

# 打一个 v* 版本标签(例:git tag v0.2.0 && git push --tags)就会:
# 在 Windows / macOS / Linux 各构建一份桌面 app,压缩,发布到一个 GitHub Release。
# 也可在 Actions 页手动触发(workflow_dispatch)只构建、不发布。
on:
push:
tags: ["v*"]
workflow_dispatch:

permissions:
contents: write # 发布 Release 需要写权限

jobs:
build:
name: Build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
asset: viralens-windows-x64.zip
- os: macos-latest
asset: viralens-macos-arm64.zip
- os: ubuntu-latest
asset: viralens-linux-x64.zip
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: "3.12" # 各重依赖在 3.12 上 wheel 最全、最稳

- name: Install deps + PyInstaller
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install pyinstaller

# 原生窗口后端只在 Windows/macOS 装(用系统自带 webview)。Linux 的 GTK webview
# 打包困难,故不装 —— Linux 包会自动回退到「开浏览器」(见 viralens_app.py)。
- name: Install native-window backend (Windows/macOS only)
if: runner.os != 'Linux'
run: python -m pip install pywebview

- name: Build (PyInstaller, onedir)
run: python -m PyInstaller --noconfirm --clean packaging/viralens.spec

- name: Package (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: Compress-Archive -Path dist/viralens -DestinationPath ${{ matrix.asset }}

- name: Package (macOS) # ditto 是 mac 上正确打包 .app 的方式(保留符号链接/权限)
if: runner.os == 'macOS'
run: ditto -c -k --keepParent dist/viralens.app ${{ matrix.asset }}

- name: Package (Linux) # zip 保留可执行位,解压后 ./viralens/viralens 直接能跑
if: runner.os == 'Linux'
run: (cd dist && zip -ry ../${{ matrix.asset }} viralens)

- uses: actions/upload-artifact@v4
with:
name: ${{ matrix.asset }}
path: ${{ matrix.asset }}
if-no-files-found: error

release:
name: Publish Release
needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true

- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: artifacts/*.zip
generate_release_notes: true
body: |
## viralens 桌面版下载

按你的系统选一个,**下载 → 解压 → 双击即用**,无需自己装 Python。

| 系统 | 文件 | 怎么打开 |
|---|---|---|
| Windows | `viralens-windows-x64.zip` | 解压后运行文件夹里的 `viralens.exe` |
| macOS (Apple Silicon) | `viralens-macos-arm64.zip` | 解压得 `viralens.app`,见下方 macOS 首次打开说明 |
| Linux (x64) | `viralens-linux-x64.zip` | 解压后运行 `./viralens/viralens` |

> **首次打开提示是正常的 —— 因为这个 app 没有花钱做苹果/微软的代码签名,不是有问题。**
> - **Windows**:若弹出「Windows 已保护你的电脑」,点 **更多信息 → 仍要运行**。
> - **macOS**:先试 **右键 → 打开**;若提示「已损坏 / 无法验证开发者」(macOS 15 Sequoia 起常见),改去 **系统设置 → 隐私与安全性**,在底部点「**仍要打开**」;或在终端执行 `xattr -dr com.apple.quarantine /把/viralens.app/拖进来` 后再双击。

启动后会直接弹出 **viralens 应用窗口**(没有终端、没有浏览器标签页;没装系统 WebView 的部分 Linux 环境会自动回退到用浏览器打开)。首次在界面里填入你的 **B站 SESSDATA** 和/或 **YouTube API key** 即可开始抓取分析。

> 可选功能「下视频分析开场镜头 + 配乐」需要自行安装 **ffmpeg**(其余抓取 / 分析 / 报告功能都无需它)。
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<img src="banner.svg" alt="viralens" width="100%">

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

## Run it on *your* creators

### Easiest: download the app — no Python, no install

Grab the build for your OS from the **[latest release](https://github.com/HarryXin0919/viralens/releases/latest)**,
unzip, and double-click. It opens in **its own app window** — no terminal, no browser tab; nothing leaves your machine.
(On a Linux box without a system WebView, it falls back to opening the UI in your browser.)

| OS | File | Open it |
|---|---|---|
| **Windows** | `viralens-windows-x64.zip` | unzip → run `viralens\viralens.exe` |
| **macOS** (Apple Silicon) | `viralens-macos-arm64.zip` | unzip → `viralens.app` (see first-launch note below) |
| **Linux** (x64) | `viralens-linux-x64.zip` | unzip → run `./viralens/viralens` |

> **The first-launch security prompt is expected** — the app isn't code-signed (that needs paid Apple/Microsoft certificates), it isn't broken.
> - **Windows:** if you see *"Windows protected your PC"*, click **More info → Run anyway**.
> - **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.

On first launch, paste your **Bilibili SESSDATA** and/or free **YouTube API key** right in the UI.
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.
The optional *opening-shots + BGM* analysis needs [ffmpeg](https://ffmpeg.org) installed; everything else works without it.

### From source (for developers)

```bash
git clone https://github.com/HarryXin0919/viralens.git
cd viralens
Expand Down Expand Up @@ -248,6 +271,7 @@ rate-limit · small *n* is reported as *"weak signal,"* never dressed up as proo
- [x] Cross-language extension to English YouTube (Entertainment-YT, 4 creators) — done; held without a counter-case
- [x] One-command front door — `python viralens.py` (just the data → CSV/JSON) · `--report` (data + full analysis + report)
- [x] Self-contained interactive HTML report — `reports/index.html`
- [x] Downloadable desktop app for Windows / macOS / Linux — no Python install needed ([releases](https://github.com/HarryXin0919/viralens/releases/latest))
- [ ] Per-creator (not keyword-based) signature-form definition
- [ ] Opt-in LLM layer for qualitative "why this form works" summaries

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

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

想从源码跑(开发者):见上方 **Run it on your creators** —— 改 `scripts/creators.py` 填你想看的任意 B站 / YouTube 创作者,
然后 `python scripts/viralens.py`(只要数据)或 `python scripts/viralens.py --report`(数据 + 分析);
也可 `python scripts/app.py` 开本地网页界面。

---

Expand Down
33 changes: 33 additions & 0 deletions packaging/build-local.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# viralens · 本地构建 Windows 桌面 app(给开发者验证用;正式三平台包由 GitHub Actions 出)
#
# 用法: powershell -ExecutionPolicy Bypass -File packaging\build-local.ps1
# 产物: dist\viralens\viralens.exe (整个 dist\viralens\ 文件夹即可压缩分发)
#
# 需要本机已装 Python 3.10+(3.12 最稳)。脚本会装好打包所需依赖,再跑 PyInstaller。
$ErrorActionPreference = "Stop"
Set-Location (Split-Path $PSScriptRoot -Parent) # 切到仓库根

# 找 Python:优先 py -3,退而求其次 python
$py = $null
if (Get-Command py -ErrorAction SilentlyContinue) { $py = "py"; $pyArgs = @("-3") }
elseif (Get-Command python -ErrorAction SilentlyContinue) { $py = "python"; $pyArgs = @() }
else { Write-Error "没找到 Python。先装 Python 3.10+ 并勾选 Add to PATH。"; exit 1 }

Write-Host "[*] 用解释器:" -NoNewline; & $py @pyArgs --version

Write-Host "[*] 安装运行依赖(来自 pyproject)+ 原生窗口后端 + PyInstaller ..."
& $py @pyArgs -m pip install --upgrade pip
& $py @pyArgs -m pip install -e ".[gui]" # [gui] = pywebview,双击弹原生窗口
& $py @pyArgs -m pip install pyinstaller

Write-Host "[*] 打包(onedir)..."
& $py @pyArgs -m PyInstaller --noconfirm --clean packaging/viralens.spec

$exe = Join-Path (Get-Location) "dist\viralens\viralens.exe"
if (Test-Path $exe) {
Write-Host "[OK] 构建完成 -> $exe"
Write-Host " 双击 viralens.exe 即可启动;整个 dist\viralens\ 文件夹打包(zip)就能分发。"
} else {
Write-Error "构建结束但没找到 $exe —— 看上面的 PyInstaller 日志。"
exit 1
}
118 changes: 118 additions & 0 deletions packaging/viralens.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# -*- mode: python ; coding: utf-8 -*-
"""
PyInstaller 打包配方:把 viralens 连同一个真正的 Python 解释器冻进一个文件夹,
用户无需自己装 Python / pip 依赖,双击即用。三平台(Win/Mac/Linux)同一份 spec。

本地构建(Windows): py -m PyInstaller --noconfirm --clean packaging/viralens.spec
产物: dist/viralens/viralens(.exe) ← onedir,整个文件夹打包分发

为什么用 onedir 而不是 onefile:流水线一次运行会让 app 自己重新拉起 ~6 次
(fetch → 各分析步骤)。onefile 每次启动都要把上百 MB 解压到临时目录,会非常慢;
onedir 直接就地运行,子步骤秒起。
"""
import os
import sys
from PyInstaller.utils.hooks import collect_all

REPO = os.path.dirname(SPECPATH) # SPECPATH 由 PyInstaller 注入 = packaging/
SCRIPTS = os.path.join(REPO, "scripts")
ENTRY = os.path.join(SPECPATH, "viralens_app.py")
ICON = os.path.join(SPECPATH, "icon.ico") # 可选;不存在就不用

datas, binaries, hiddenimports = [], [], []

# —— 重依赖:连子模块 + 数据文件(jieba 词典、bilibili_api 资源等)一起收 ——
for pkg in ("bilibili_api", "aiohttp", "jieba"):
d, b, h = collect_all(pkg)
datas += d
binaries += b
hiddenimports += h

# matplotlib / numpy / Pillow 自带 PyInstaller hook,会自动带数据;这里补 Agg 后端保险
hiddenimports += ["matplotlib.backends.backend_agg", "numpy", "PIL"]

# 原生窗口后端(可选):装了 pywebview 就连同其平台后端一起收进来,实现「双击弹原生窗口」;
# 没装(比如 Linux 不打包 webview)则打成「开浏览器」回退版 —— 入口 viralens_app.py 会自动判断。
try:
import webview # noqa: F401
_d, _b, _h = collect_all("webview")
datas += _d
binaries += _b
hiddenimports += _h
except Exception:
pass

# —— 项目自己的脚本 ——
# app 懒加载它们、viralens 通过 `--vl-exec <模块名>` 用 runpy 跑它们,
# 静态分析有可能看不全,这里全部显式声明,确保都被冻进去。
hiddenimports += [
"runtime", "app", "viralens",
"fetch_multi", "fetch_bilibili", "fetch_youtube",
"compare_form", "creator_profile", "scan_signals", "charts", "export_data", "build_report",
"diagnose", "analyze_video", "import_private",
"creators", "features", "benchmarks",
"classify_and_stats", "comments", "compare_meme", "fetch_covers",
"fetch_videos", "resolve_creators", "subtitle",
]

# —— 只读资源:网页界面 + 配置模板 —— 放进打包根目录,app.py 用 runtime.ASSET_DIR 找它们 ——
datas += [
(os.path.join(SCRIPTS, "gui.html"), "."),
(os.path.join(SCRIPTS, "diagnose.html"), "."),
(os.path.join(SCRIPTS, "config_local.example.py"), "."),
]

a = Analysis(
[ENTRY],
pathex=[SCRIPTS], # 让 import runtime / app / 各脚本 找得到
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
runtime_hooks=[],
# yt_dlp 体积巨大且只服务于「下 YouTube 视频开头」这个可选功能(缺了会优雅降级);
# tkinter 是 GUI 工具包,我们用 matplotlib 的 Agg 后端、用不到它。
# 注意:不要排 unittest/test —— matplotlib→pyparsing.testing 会在导入时 import unittest。
excludes=["yt_dlp", "tkinter"],
noarchive=False,
)

pyz = PYZ(a.pure)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name="viralens",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=False, # 窗口模式:不弹终端。子进程 stdout 由 viralens_app._ensure_std() 接回管道
icon=(ICON if os.path.exists(ICON) else None),
)

coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=False,
name="viralens",
)

# macOS:在 onedir 外再包一层 .app,双击即用(无终端窗口)。
if sys.platform == "darwin":
app_bundle = BUNDLE(
coll,
name="viralens.app",
icon=(ICON if os.path.exists(ICON) else None),
bundle_identifier="dev.harryxin.viralens",
info_plist={
"CFBundleName": "viralens",
"CFBundleDisplayName": "viralens",
"NSHighResolutionCapable": True,
"LSBackgroundOnly": False,
},
)
66 changes: 66 additions & 0 deletions packaging/viralens_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
viralens · 打包成桌面 app 时的入口(被 PyInstaller 冻结,见 viralens.spec)。

目标:双击直接弹出一个**原生应用窗口**(pywebview)——没有终端、没有浏览器标签页。
缺原生 webview 运行时(如部分 Linux 没装 WebKitGTK)时,自动回退到「开浏览器」。

两种角色,靠命令行第一个参数区分:
· 正常双击启动 → 起本地服务器 + 开原生窗口(或回退浏览器)。
· 自己重新拉起 → 形如 `viralens --vl-exec fetch_multi --force`:流水线某一步,
由 runtime.dispatch_if_worker() 接管、当成 __main__ 跑掉再退出(不开窗口)。

注意:本 app 以「窗口模式」(console=False)打包,没有控制台。下面 _ensure_std() 负责:
· 子步骤进程(被父进程用管道收 stdout 显示进度)→ 把标准输出接回那条管道;
· 主窗口进程(双击,没有任何 std 句柄)→ 接到 devnull,避免 print 崩溃。
"""
import io
import os
import sys


def _ensure_std():
"""窗口模式下 sys.stdout/stderr 可能是 None。子进程的句柄是父进程给的管道(有效)→
接回去让进度能被收集;主窗口进程没有有效句柄 → dup 失败,退到 devnull。"""
for name, fd in (("stdout", 1), ("stderr", 2)):
if getattr(sys, name, None) is None:
try:
stream = io.TextIOWrapper(os.fdopen(os.dup(fd), "wb"),
encoding="utf-8", errors="replace", line_buffering=True)
except Exception:
stream = open(os.devnull, "w", encoding="utf-8", errors="replace")
setattr(sys, name, stream)


_ensure_std()

import runtime

runtime.bootstrap()
runtime.dispatch_if_worker() # 若是 --vl-exec 子步骤:跑完即退出,绝不往下走(不开窗口)

import app

# —— 主进程:起服务器,开原生窗口;开不出来就回退浏览器 ——
srv, url = app.start_server()

_opened = False
try:
import webview
webview.create_window("viralens", url, width=1180, height=820, min_size=(900, 600))
webview.start() # 阻塞,直到用户关掉窗口
_opened = True
except Exception:
_opened = False

if not _opened:
# 没有可用的原生 webview(如 Linux 缺 WebKitGTK)→ 开浏览器并挂住进程
import threading
import webbrowser
try:
webbrowser.open(url)
except Exception:
pass
try:
threading.Event().wait()
except KeyboardInterrupt:
pass
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ dependencies = [
"numpy>=1.23.0",
]

# 仅在「打包成桌面 app」时需要:原生窗口后端。源码 CLI / 网页界面都不依赖它。
[project.optional-dependencies]
gui = ["pywebview>=5.0"]

[project.urls]
Homepage = "https://github.com/HarryXin0919/viralens"
Repository = "https://github.com/HarryXin0919/viralens"
Expand Down
Loading
Loading