Skip to content

Commit 85fa054

Browse files
committed
Merge origin/main into release artifact scan workflow branch
2 parents 16fde1a + dc890be commit 85fa054

11 files changed

Lines changed: 458 additions & 8 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: Privacy Guard
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
scan:
14+
runs-on: ubuntu-latest
15+
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v4
19+
20+
- name: Ensure sensitive local settings are not tracked
21+
shell: bash
22+
run: |
23+
set -euo pipefail
24+
if git ls-files --error-unmatch config/settings.json >/dev/null 2>&1; then
25+
echo "❌ config/settings.json must never be tracked"
26+
exit 1
27+
fi
28+
29+
- name: Ensure build spec does not bundle config directory
30+
shell: bash
31+
run: |
32+
set -euo pipefail
33+
if grep -q "('config', 'config')" VinylFlow.spec; then
34+
echo "❌ VinylFlow.spec must not bundle config directory"
35+
exit 1
36+
fi
37+
38+
- name: Scan tracked text files for personal data patterns
39+
shell: bash
40+
run: |
41+
set -euo pipefail
42+
python - <<'PY'
43+
import pathlib
44+
import re
45+
import subprocess
46+
import sys
47+
48+
tracked = subprocess.check_output(["git", "ls-files"], text=True).splitlines()
49+
50+
patterns = [
51+
(
52+
"Hardcoded Discogs token",
53+
re.compile(r"DISCOGS_USER_TOKEN\s*[:=]\s*['\"]?(?!your_token_here\b)[A-Za-z0-9]{20,}")
54+
),
55+
(
56+
"macOS personal user path",
57+
re.compile(r"/Users/(?!you\b)[A-Za-z0-9._-]+")
58+
),
59+
(
60+
"Windows personal user path",
61+
re.compile(r"C:\\\\Users\\\\(?!you\b)[A-Za-z0-9._-]+")
62+
),
63+
]
64+
65+
allowed_files = {
66+
".env.example",
67+
}
68+
69+
failures = []
70+
for rel in tracked:
71+
if rel in allowed_files:
72+
continue
73+
p = pathlib.Path(rel)
74+
if not p.exists() or p.is_dir():
75+
continue
76+
77+
try:
78+
text = p.read_text(encoding="utf-8")
79+
except Exception:
80+
continue
81+
82+
for name, regex in patterns:
83+
for m in regex.finditer(text):
84+
line = text.count("\n", 0, m.start()) + 1
85+
failures.append((rel, line, name, m.group(0)[:200]))
86+
87+
if failures:
88+
print("❌ Privacy guard found potential leaks:")
89+
for rel, line, name, snippet in failures:
90+
print(f"- {rel}:{line}: {name}: {snippet}")
91+
sys.exit(1)
92+
93+
print("✅ Privacy guard passed")
94+
PY
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
name: Build Windows Release Asset
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tag:
7+
description: "Release tag to update (e.g. v0.2.0-beta6)"
8+
required: true
9+
default: "v0.2.0-beta6"
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
build-and-upload:
16+
runs-on: windows-latest
17+
18+
steps:
19+
- name: Checkout repository
20+
uses: actions/checkout@v4
21+
22+
- name: Set up Python 3.11
23+
uses: actions/setup-python@v5
24+
with:
25+
python-version: "3.11"
26+
27+
- name: Install FFmpeg
28+
shell: powershell
29+
run: |
30+
choco install ffmpeg -y
31+
ffmpeg -version
32+
33+
- name: Install Python dependencies
34+
shell: powershell
35+
run: |
36+
python -m pip install --upgrade pip
37+
pip install -r requirements.txt
38+
pip install pyinstaller
39+
40+
- name: Build Windows app
41+
shell: powershell
42+
run: |
43+
pyinstaller --clean -y VinylFlow.spec
44+
45+
- name: Package unsigned Windows app
46+
shell: powershell
47+
run: |
48+
if (Test-Path VinylFlow-windows-unsigned.zip) { Remove-Item VinylFlow-windows-unsigned.zip -Force }
49+
Compress-Archive -Path dist\VinylFlow\* -DestinationPath VinylFlow-windows-unsigned.zip
50+
Get-Item VinylFlow-windows-unsigned.zip | Format-List Name,Length,LastWriteTime
51+
52+
- name: Replace release asset
53+
env:
54+
GH_TOKEN: ${{ github.token }}
55+
shell: powershell
56+
run: |
57+
gh release upload ${{ inputs.tag }} VinylFlow-windows-unsigned.zip --clobber
58+
$release = gh release view ${{ inputs.tag }} --json assets | ConvertFrom-Json
59+
$asset = $release.assets | Where-Object { $_.name -eq 'VinylFlow-windows-unsigned.zip' }
60+
$asset | Select-Object name,size,updatedAt,url | Format-List

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ Turn your vinyl recordings into perfectly tagged, organized digital files in min
88

99
---
1010

11+
## Use VinylFlow
12+
13+
- 🖥️ **Desktop apps:** install builds from [vinylflow.app/install](https://vinylflow.app/install)
14+
- 🐳 **Docker (self-hosted):** run locally via [Quick Start (Docker)](#quick-start-docker)
15+
- ⚙️ **Python local mode:** see [Manual Setup (Non-Docker)](#manual-setup-non-docker)
16+
17+
---
18+
1119
## The Problem
1220

1321
Digitizing a vinyl record manually takes **20–30 minutes per album**: record in Audacity, manually find track boundaries, split, export, look up metadata, type it all in, find cover art, embed it. Multiply that by a collection of hundreds of records and it's a weekend project that never ends.
@@ -96,13 +104,23 @@ When you open VinylFlow for the first time, you'll see a welcome screen that gui
96104

97105
## Desktop Apps (Beta)
98106

99-
VinylFlow also has desktop app builds for macOS and Windows in a beta track.
107+
VinylFlow desktop apps are available for macOS and Windows (beta track).
108+
109+
- Installer downloads: [vinylflow.app/install](https://vinylflow.app/install)
100110

101111
- Desktop beta work is published from `desktop-beta`
102112
- `main` remains the stable Docker-first channel
103113
- For local desktop mode, run: `python desktop_launcher.py`
104114
- Packaging/release scripts are in `scripts/`
105115

116+
### Developer Branch Note
117+
118+
To avoid accidental promotion of beta app work:
119+
120+
- Open desktop feature PRs into `desktop-beta`
121+
- Keep `main` for stable/docs/release-safe changes
122+
- Promote desktop work to `main` only when explicitly ready
123+
106124
For full branching and release details, see [docs/BRANCHING_STRATEGY.md](docs/BRANCHING_STRATEGY.md).
107125

108126
---

VinylFlow.spec

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# -*- mode: python ; coding: utf-8 -*-
2+
3+
import shutil
4+
import sys
5+
from pathlib import Path
6+
7+
8+
WINDOWS_ICON = 'assets/VinylFlow.ico' if sys.platform.startswith('win') else None
9+
FFMPEG_PATH = shutil.which('ffmpeg')
10+
11+
if not FFMPEG_PATH:
12+
raise RuntimeError('ffmpeg was not found on PATH. Install ffmpeg before building.')
13+
14+
DATA_FILES = [('backend/static', 'backend/static')]
15+
16+
17+
a = Analysis(
18+
['desktop_launcher.py'],
19+
pathex=[],
20+
binaries=[(FFMPEG_PATH, 'ffmpeg_bin')],
21+
datas=DATA_FILES,
22+
hiddenimports=[
23+
'backend.api',
24+
'webview',
25+
'webview.platforms.cocoa',
26+
'webview.platforms.winforms',
27+
],
28+
hookspath=[],
29+
hooksconfig={},
30+
runtime_hooks=[],
31+
excludes=[],
32+
noarchive=False,
33+
optimize=0,
34+
)
35+
pyz = PYZ(a.pure)
36+
37+
exe = EXE(
38+
pyz,
39+
a.scripts,
40+
[],
41+
exclude_binaries=True,
42+
name='VinylFlow',
43+
debug=False,
44+
bootloader_ignore_signals=False,
45+
strip=False,
46+
upx=True,
47+
console=False,
48+
disable_windowed_traceback=False,
49+
argv_emulation=False,
50+
target_arch=None,
51+
codesign_identity=None,
52+
entitlements_file=None,
53+
icon=WINDOWS_ICON,
54+
)
55+
coll = COLLECT(
56+
exe,
57+
a.binaries,
58+
a.datas,
59+
strip=False,
60+
upx=True,
61+
upx_exclude=[],
62+
name='VinylFlow',
63+
)
64+
app = BUNDLE(
65+
coll,
66+
name='VinylFlow.app',
67+
icon='assets/VinylFlow.icns',
68+
bundle_identifier=None,
69+
)

assets/VinylFlow.icns

89.3 KB
Binary file not shown.

assets/VinylFlow.ico

12.8 KB
Binary file not shown.

backend/api.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class ConfigUpdate(BaseModel):
182182
silence_threshold: Optional[float] = None
183183
min_silence_duration: Optional[float] = None
184184
min_track_length: Optional[float] = None
185+
output_dir: Optional[str] = None
185186

186187

187188
class DiscogsSetupRequest(BaseModel):
@@ -873,6 +874,7 @@ async def get_config():
873874
"min_silence_duration": audio_processor.min_silence_duration,
874875
"min_track_length": audio_processor.min_track_length,
875876
"flac_compression": audio_processor.flac_compression,
877+
"output_dir": config.default_output_dir,
876878
}
877879

878880

@@ -885,6 +887,10 @@ async def update_config(updates: ConfigUpdate):
885887
audio_processor.min_silence_duration = updates.min_silence_duration
886888
if updates.min_track_length is not None:
887889
audio_processor.min_track_length = updates.min_track_length
890+
if updates.output_dir is not None:
891+
output_dir = str(Path(updates.output_dir).expanduser())
892+
config.default_output_dir = output_dir
893+
config.save_output_dir(output_dir)
888894

889895
return await get_config()
890896

backend/static/app.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ function vinylApp() {
7575
silence_threshold: -40,
7676
min_silence_duration: 1.5,
7777
min_track_length: 30,
78-
flac_compression: 8
78+
flac_compression: 8,
79+
output_dir: ''
7980
},
8081

8182
// Supported input file extensions
@@ -290,12 +291,10 @@ function vinylApp() {
290291
*/
291292
async saveConfig() {
292293
try {
293-
// Don't send output_dir to API
294-
const { output_dir, ...configToSave } = this.config;
295294
const response = await fetch('/api/config', {
296295
method: 'PUT',
297296
headers: { 'Content-Type': 'application/json' },
298-
body: JSON.stringify(configToSave)
297+
body: JSON.stringify(this.config)
299298
});
300299
const data = await response.json();
301300
this.config = data;
@@ -307,6 +306,26 @@ function vinylApp() {
307306
}
308307
},
309308

309+
/**
310+
* Open desktop folder picker for output directory
311+
*/
312+
async chooseOutputFolder() {
313+
try {
314+
if (!window.pywebview || !window.pywebview.api || !window.pywebview.api.select_output_folder) {
315+
alert('Folder picker is available in the desktop app only.');
316+
return;
317+
}
318+
319+
const selected = await window.pywebview.api.select_output_folder(this.config.output_dir || '');
320+
if (selected) {
321+
this.config.output_dir = selected;
322+
}
323+
} catch (error) {
324+
console.error('Failed to choose output folder:', error);
325+
alert('Failed to open folder picker');
326+
}
327+
},
328+
310329
/**
311330
* Check if a file has a supported extension
312331
*/

backend/static/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,13 @@ <h2 class="text-2xl font-bold text-primary mb-4">Settings</h2>
319319
<label class="block text-sm font-medium text-secondary mb-1">Min Silence Duration (sec)</label>
320320
<input type="number" step="0.1" x-model="config.min_silence_duration" class="w-full px-3 py-2 border focus-primary rounded-md" style="border-color: #8A8A86;">
321321
</div>
322+
<div>
323+
<label class="block text-sm font-medium text-secondary mb-1">Output Folder</label>
324+
<div class="flex gap-2">
325+
<input type="text" x-model="config.output_dir" class="flex-1 px-3 py-2 border focus-primary rounded-md" style="border-color: #8A8A86;" placeholder="/Users/you/Music/VinylFlow">
326+
<button @click="chooseOutputFolder()" type="button" class="px-3 py-2 rounded-md text-primary hover:bg-page" style="background-color: #F0EFEB;">Browse</button>
327+
</div>
328+
</div>
322329

323330
<!-- Discogs Token Section -->
324331
<div class="border-t pt-4 mt-4" style="border-color: #8A8A86;">

0 commit comments

Comments
 (0)