Skip to content

Commit cce38cb

Browse files
committed
Release v0.1.6
1 parent 29eb771 commit cce38cb

File tree

15 files changed

+793
-122
lines changed

15 files changed

+793
-122
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
|:-:|:-:|
3737
| ![Settings](docs/screenshots/setting.png) | ![macOS](docs/screenshots/macos.png) |
3838

39-
## What's New In v0.1.5
39+
## What's New In v0.1.6
4040

41-
`v0.1.5` rolls the experimental multi-axis work into the main release with official `FunOSR (Direct Serial / COM)` support, `Intiface / Buttplug` raw TCode transport, adjustable one-line `L0/L1/L2/R0/R1/R2` output, smoother timeline and heatmap playback updates, better Handy re-sync after timeline seeks, and the packaged Windows fixes from `exp.7` / `exp.8`.
41+
`v0.1.6` adds quick `STR` stroke controls in the playback bar, automatic skipping for long empty script gaps in sparse audio scripts, stronger media duration handling, smoother seek / scrub behavior, and faster large-folder scanning with safer directory traversal.
4242

43-
| v0.1.5 Preview |
43+
| v0.1.6 Preview |
4444
|:-:|
45-
| ![v0.1.5 Preview](docs/screenshots/preview_v015_exp1.png) |
45+
| ![v0.1.6 Preview](docs/screenshots/preview_v015_exp1.png) |
4646

47-
- Download the stable release: [ScriptPlayer+ v0.1.5](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5)
47+
- Download the stable release: [ScriptPlayer+ v0.1.6](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.6)
4848

4949
## Features
5050

@@ -78,7 +78,7 @@
7878

7979
1. Download the latest Windows x64 build from [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)
8080
2. Extract and run `ScriptPlayerPlus.exe` — no installation required
81-
3. The main `v0.1.5` build includes The Handy, Intiface / Buttplug, and official FunOSR device support
81+
3. The main `v0.1.6` build includes The Handy, Intiface / Buttplug, and official FunOSR device support
8282

8383
### macOS
8484

docs/README_JA.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
|:-:|:-:|
3737
| ![設定](screenshots/setting.png) | ![macOS](screenshots/macos.png) |
3838

39-
## v0.1.5 の新機能
39+
## v0.1.6 の新機能
4040

41-
`v0.1.5` では実験版で進めていたマルチアクシス対応を正式版へ取り込み、公式 `FunOSR (Direct Serial / COM)` サポート、`Intiface / Buttplug` の raw TCode 転送、調整可能な `L0/L1/L2/R0/R1/R2` の 1 行出力、より滑らかなタイムライン / ヒートマップ追従、seek 後の Handy 再同期改善、そして `exp.7` / `exp.8` で入った Windows パッケージ版の修正を含みます
41+
`v0.1.6` では再生バーから直接調整できる `STR` ストローク範囲コントロール、空白の長いスクリプト区間の自動スキップ、より安定したメディア長の認識、より滑らかな seek / スクラブ動作、そして大きなフォルダのスキャンとディレクトリ走査の安定化を追加しました
4242

43-
| v0.1.5 プレビュー |
43+
| v0.1.6 プレビュー |
4444
|:-:|
45-
| ![v0.1.5 プレビュー](screenshots/preview_v015_exp1.png) |
45+
| ![v0.1.6 プレビュー](screenshots/preview_v015_exp1.png) |
4646

47-
- 正式リリースのダウンロード: [ScriptPlayer+ v0.1.5](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5)
47+
- 正式リリースのダウンロード: [ScriptPlayer+ v0.1.6](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.6)
4848

4949
## 主な機能
5050

docs/README_KO.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
|:-:|:-:|
3737
| ![설정](screenshots/setting_kor.png) | ![macOS](screenshots/macos.png) |
3838

39-
## v0.1.5에서 추가된 내용
39+
## v0.1.6에서 추가된 내용
4040

41-
`v0.1.5`는 실험판에서 검증한 다축 기능을 정식 릴리스에 통합해 공식 `FunOSR (Direct Serial / COM)` 지원, `Intiface / Buttplug` raw TCode 전송, 조절 가능한 `L0/L1/L2/R0/R1/R2` 한 줄 출력, 더 부드러운 타임라인/히트맵 추적, seek 후 Handy 재동기화 개선, 그리고 `exp.7` / `exp.8`에서 들어간 Windows 패키징 안정화까지 포함합니다.
41+
`v0.1.6`은 재생바에서 바로 조절하는 `STR` 스트로크 범위 컨트롤, 비어 있는 스크립트 구간 자동 스킵, 더 안정적인 미디어 길이 인식, 더 부드러운 seek / 스크럽 동작, 그리고 대형 폴더 스캔 및 디렉터리 순회 안정화까지 포함합니다.
4242

43-
| v0.1.5 미리보기 |
43+
| v0.1.6 미리보기 |
4444
|:-:|
45-
| ![v0.1.5 미리보기](screenshots/preview_v015_exp1.png) |
45+
| ![v0.1.6 미리보기](screenshots/preview_v015_exp1.png) |
4646

47-
- 정식 릴리스 다운로드: [ScriptPlayer+ v0.1.5](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5)
47+
- 정식 릴리스 다운로드: [ScriptPlayer+ v0.1.6](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.6)
4848

4949
## 주요 기능
5050

@@ -78,7 +78,7 @@
7878

7979
1. [Releases](https://github.com/sioaeko/scriptplayer-plus/releases)에서 최신 Windows x64 빌드 다운로드
8080
2. 압축 해제 후 `ScriptPlayerPlus.exe` 실행 — 설치 불필요
81-
3. 메인 `v0.1.5` 빌드에 The Handy, Intiface / Buttplug, 공식 FunOSR 지원이 모두 포함됩니다
81+
3. 메인 `v0.1.6` 빌드에 The Handy, Intiface / Buttplug, 공식 FunOSR 지원이 모두 포함됩니다
8282

8383
### macOS
8484

docs/README_ZH.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
|:-:|:-:|
3737
| ![设置](screenshots/setting.png) | ![macOS](screenshots/macos.png) |
3838

39-
## v0.1.5 新增内容
39+
## v0.1.6 新增内容
4040

41-
`v0.1.5` 正式版整合了实验阶段的多轴功能,包含官方 `FunOSR (Direct Serial / COM)` 支持、`Intiface / Buttplug` 原始 TCode 传输、可调节的 `L0/L1/L2/R0/R1/R2` 单行输出、更顺滑的时间线 / 热力图跟随、seek 后更稳定的 Handy 重新同步,以及 `exp.7` / `exp.8` 的 Windows 打包修复
41+
`v0.1.6` 增加了可在播放栏直接调整的 `STR` 行程范围控制、针对空白较长脚本区段的自动跳过、更稳定的媒体时长识别、更顺滑的 seek / 拖动体验,以及对大型文件夹扫描和目录遍历稳定性的改进
4242

43-
| v0.1.5 预览 |
43+
| v0.1.6 预览 |
4444
|:-:|
45-
| ![v0.1.5 预览](screenshots/preview_v015_exp1.png) |
45+
| ![v0.1.6 预览](screenshots/preview_v015_exp1.png) |
4646

47-
- 下载正式版本: [ScriptPlayer+ v0.1.5](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.5)
47+
- 下载正式版本: [ScriptPlayer+ v0.1.6](https://github.com/sioaeko/scriptplayer-plus/releases/tag/v0.1.6)
4848

4949
## 主要功能
5050

electron/main.ts

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,58 @@ const SUBTITLE_DIR_KEYWORDS = [
4242
]
4343
const MAX_SUBTITLE_SEARCH_DEPTH = 2
4444
const MAX_SCAN_SUBTITLE_VALIDATION_CANDIDATES = 3
45+
const MIN_SCAN_SUBTITLE_SCORE = 900
4546
const SCAN_YIELD_INTERVAL = 25
4647
const NATURAL_SORTER = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
4748

4849
let mainWindow: BrowserWindow | null = null
4950
const subtitleCandidateCache = new Map<string, string[]>()
5051
const subtitleAnalysisCache = new Map<string, { content: string; hasCues: boolean } | null>()
52+
const directoryEntryNameCache = new Map<string, Set<string>>()
5153
const FUNSCRIPT_EXTS = ['.funscript', '.json']
5254
const osrSerialManager = new OsrSerialManager((state) => {
5355
mainWindow?.webContents.send('osrSerial:stateChanged', state)
5456
})
5557

58+
function normalizePathKey(targetPath: string): string {
59+
return process.platform === 'win32' ? targetPath.toLowerCase() : targetPath
60+
}
61+
62+
async function getDirectoryRealPathKey(dirPath: string): Promise<string> {
63+
try {
64+
return normalizePathKey(await fs.promises.realpath(dirPath))
65+
} catch {
66+
return normalizePathKey(dirPath)
67+
}
68+
}
69+
70+
function getDirectoryRealPathKeySync(dirPath: string): string {
71+
try {
72+
return normalizePathKey(fs.realpathSync.native(dirPath))
73+
} catch {
74+
return normalizePathKey(dirPath)
75+
}
76+
}
77+
78+
function getDirectoryEntryNameSet(dirPath: string): Set<string> {
79+
const cacheKey = normalizePathKey(dirPath)
80+
const cached = directoryEntryNameCache.get(cacheKey)
81+
if (cached) {
82+
return cached
83+
}
84+
85+
let names: string[]
86+
try {
87+
names = fs.readdirSync(dirPath)
88+
} catch {
89+
names = []
90+
}
91+
92+
const collected = new Set(names.map((name) => name.toLowerCase()))
93+
directoryEntryNameCache.set(cacheKey, collected)
94+
return collected
95+
}
96+
5697
function createWindow() {
5798
mainWindow = new BrowserWindow({
5899
width: 1280,
@@ -159,6 +200,7 @@ ipcMain.handle('fs:readDir', async (_event, dirPath: string) => {
159200
try {
160201
const files: Array<{ name: string; path: string; type: 'video' | 'audio'; hasScript: boolean; hasSubtitles: boolean; relativePath: string }> = []
161202
let scannedEntries = 0
203+
const visitedDirectories = new Set<string>()
162204

163205
const maybeYieldDuringScan = async () => {
164206
scannedEntries += 1
@@ -168,6 +210,12 @@ ipcMain.handle('fs:readDir', async (_event, dirPath: string) => {
168210
}
169211

170212
const scanDir = async (dir: string, prefix: string): Promise<void> => {
213+
const visitKey = await getDirectoryRealPathKey(dir)
214+
if (visitedDirectories.has(visitKey)) {
215+
return
216+
}
217+
visitedDirectories.add(visitKey)
218+
171219
let entries: fs.Dirent[]
172220
try {
173221
entries = await fs.promises.readdir(dir, { withFileTypes: true })
@@ -179,6 +227,10 @@ ipcMain.handle('fs:readDir', async (_event, dirPath: string) => {
179227
await maybeYieldDuringScan()
180228

181229
const fullPath = path.join(dir, entry.name)
230+
if (entry.isSymbolicLink()) {
231+
continue
232+
}
233+
182234
if (entry.isDirectory()) {
183235
await scanDir(fullPath, prefix ? prefix + '/' + entry.name : entry.name)
184236
} else if (entry.isFile()) {
@@ -306,13 +358,14 @@ const NAS_EXTS = [...MEDIA_EXTS, '.funscript']
306358
function hasBundledFunscriptsForMediaScan(mediaPath: string): boolean {
307359
const mediaDir = path.dirname(mediaPath)
308360
const mediaBaseName = path.basename(mediaPath, path.extname(mediaPath))
361+
const entryNames = getDirectoryEntryNameSet(mediaDir)
309362

310363
for (const definition of SCRIPT_AXIS_DEFINITIONS) {
311364
for (const suffix of definition.suffixes) {
312365
const fileName = suffix
313366
? `${mediaBaseName}.${suffix}.funscript`
314367
: `${mediaBaseName}.funscript`
315-
if (fs.existsSync(path.join(mediaDir, fileName))) {
368+
if (entryNames.has(fileName.toLowerCase())) {
316369
return true
317370
}
318371
}
@@ -501,6 +554,12 @@ function findSubtitleMatches(mediaPath: string, mode: 'scan' | 'full'): string[]
501554
: rankedCandidates
502555
const matches: Array<{ filePath: string; score: number }> = []
503556

557+
if (mode === 'scan') {
558+
return rankedCandidates
559+
.filter((candidate) => candidate.score >= MIN_SCAN_SUBTITLE_SCORE)
560+
.map(({ filePath }) => filePath)
561+
}
562+
504563
for (const candidate of candidatesToValidate) {
505564
const analysis = readSubtitleAnalysis(candidate.filePath)
506565
if (!analysis?.hasCues) continue
@@ -527,7 +586,8 @@ function findSubtitleMatches(mediaPath: string, mode: 'scan' | 'full'): string[]
527586
}
528587

529588
function collectSubtitleCandidates(rootDir: string): string[] {
530-
const cached = subtitleCandidateCache.get(rootDir)
589+
const cacheKey = normalizePathKey(rootDir)
590+
const cached = subtitleCandidateCache.get(cacheKey)
531591
if (cached) {
532592
return cached
533593
}
@@ -536,8 +596,9 @@ function collectSubtitleCandidates(rootDir: string): string[] {
536596
const visited = new Set<string>()
537597

538598
const walk = (currentDir: string, depth: number, matchedKeyword: boolean) => {
539-
if (visited.has(currentDir)) return
540-
visited.add(currentDir)
599+
const visitKey = getDirectoryRealPathKeySync(currentDir)
600+
if (visited.has(visitKey)) return
601+
visited.add(visitKey)
541602

542603
let entries: fs.Dirent[]
543604
try {
@@ -557,7 +618,7 @@ function collectSubtitleCandidates(rootDir: string): string[] {
557618
if (depth >= MAX_SUBTITLE_SEARCH_DEPTH) return
558619

559620
for (const entry of entries) {
560-
if (!entry.isDirectory()) continue
621+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue
561622
const nextMatchedKeyword = matchedKeyword || directoryLooksLikeSubtitle(entry.name)
562623
const shouldDescend = depth === 0 || nextMatchedKeyword
563624
if (!shouldDescend) continue
@@ -567,7 +628,7 @@ function collectSubtitleCandidates(rootDir: string): string[] {
567628

568629
walk(rootDir, 0, false)
569630
const collected = Array.from(results)
570-
subtitleCandidateCache.set(rootDir, collected)
631+
subtitleCandidateCache.set(cacheKey, collected)
571632
return collected
572633
}
573634

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "scriptplayer-plus",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"description": "ScriptPlayer+ - Funscript video player with Handy, Intiface, and FunOSR support",
55
"author": "sioaeko <grade0422@gmail.com>",
66
"license": "PolyForm-Noncommercial-1.0.0",

0 commit comments

Comments
 (0)