Compare commits

..

5 Commits

Author SHA1 Message Date
Nexmoe
b229225b6e chore: release v1.1.2 2025-12-26 13:04:52 +08:00
Nexmoe
5497ed6245 chore(templates): simplify bug report (#57) 2025-12-26 13:04:30 +08:00
Nexmoe
155bf652f2 fix(ci): harden download steps (#56) 2025-12-26 13:02:21 +08:00
Nexmoe
86bd77d995 fix(download): improve format fallbacks (#55) 2025-12-26 13:02:07 +08:00
Nexmoe
b38b356c22 feat(issue): add issue templates (#54) 2025-12-26 12:53:51 +08:00
8 changed files with 150 additions and 51 deletions

37
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Bug Report
description: Report a problem or regression
title: "[Bug]: "
labels:
- bug
body:
- type: input
id: app_version
attributes:
label: App version
description: The VidBee version (e.g., 1.2.3)
placeholder: 1.2.3
validations:
required: true
- type: input
id: os_version
attributes:
label: OS version
description: Your operating system and version (e.g., macOS 14.2, Windows 11 23H2)
placeholder: macOS 14.2
validations:
required: true
- type: textarea
id: actual
attributes:
label: Observed behavior
placeholder: What actually happened
validations:
required: true
- type: textarea
id: logs
attributes:
label: Logs or screenshots
description: Paste relevant logs or add screenshots
placeholder: Attach files or paste logs here
validations:
required: false

View File

@@ -0,0 +1,38 @@
name: Feature Request
description: Suggest an idea or improvement
title: "[Feature]: "
labels:
- enhancement
body:
- type: textarea
id: problem
attributes:
label: Problem to solve
description: What problem are you trying to solve?
placeholder: I want to...
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: Describe the feature or change you want
placeholder: It would be great if...
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives considered
description: Other solutions or workarounds you considered
placeholder: I tried...
validations:
required: false
- type: textarea
id: extra
attributes:
label: Additional context
description: Add any other context or screenshots
placeholder: Links, screenshots, or related issues
validations:
required: false

View File

@@ -76,10 +76,10 @@ jobs:
FFMPEG_OUTPUT: ${{ matrix.ffmpeg_output }}
run: |
set -euo pipefail
curl -L "${{ matrix.ffmpeg_arm_url }}" -o ffmpeg-arm.zip
curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_arm_url }}" -o ffmpeg-arm.zip
unzip -q ffmpeg-arm.zip -d ffmpeg-arm
curl -L "${{ matrix.ffmpeg_x86_url }}" -o ffmpeg-x86.zip
curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_x86_url }}" -o ffmpeg-x86.zip
unzip -q ffmpeg-x86.zip -d ffmpeg-x86
arm_bin="ffmpeg-arm/${{ matrix.ffmpeg_inner_path }}"
@@ -103,7 +103,11 @@ jobs:
shell: bash
run: |
set -euo pipefail
curl -L "${{ matrix.ffmpeg_url }}" -o ffmpeg.tar.xz
curl -fL --retry 3 --retry-delay 2 --retry-connrefused "${{ matrix.ffmpeg_url }}" -o ffmpeg.tar.xz
if ! tar -tf ffmpeg.tar.xz >/dev/null 2>&1; then
echo "::error::Downloaded ffmpeg archive is not a valid tar.xz"
exit 1
fi
mkdir ffmpeg
tar -xf ffmpeg.tar.xz -C ffmpeg
cp "ffmpeg/${{ matrix.ffmpeg_inner_path }}" "resources/${{ matrix.ffmpeg_output }}"
@@ -112,7 +116,7 @@ jobs:
- name: Download yt-dlp binary
shell: bash
run: |
curl -L "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp_asset }}" -o "resources/${{ matrix.ytdlp_output }}"
curl -fL --retry 3 --retry-delay 2 --retry-connrefused "https://github.com/yt-dlp/yt-dlp/releases/latest/download/${{ matrix.ytdlp_asset }}" -o "resources/${{ matrix.ytdlp_output }}"
if [[ "${{ matrix.platform }}" == "linux" ]] || [[ "${{ matrix.platform }}" == "macos" ]]; then
chmod +x "resources/${{ matrix.ytdlp_output }}"
fi
@@ -130,4 +134,3 @@ jobs:
name: dist-${{ matrix.os }}
path: dist/
retention-days: 1

View File

@@ -1,6 +1,6 @@
{
"scripts": {
"setup": "cp $CONDUCTOR_ROOT_PATH/resources/ resources/ && pnpm install",
"setup": "cp -r $CONDUCTOR_ROOT_PATH/resources resources/ && pnpm install",
"run": "pnpm run dev"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "vidbee",
"version": "1.1.1",
"version": "1.1.2",
"description": "A modern Electron application for downloading videos and audios",
"main": "./out/main/index.js",
"author": "VidBee",

View File

@@ -21,6 +21,10 @@ export const resolveVideoFormatSelector = (options: DownloadOptions): string =>
const format = options.format
const audioFormat = options.audioFormat
if (format && audioFormat === '') {
return format
}
if (format && (format.includes('/') || (audioFormat === undefined && format.includes('+')))) {
return format
}

View File

@@ -114,10 +114,12 @@ const buildVideoFormatPreference = (settings: AppSettings): string => {
combinations.push(video)
}
} else {
// Use bestvideo+bestaudio as fallback instead of 'best' to ensure merging
// Prefer merged formats, then allow 'best' as a compatibility fallback.
combinations.push('bestvideo+bestaudio')
}
combinations.push('best')
return dedupe(combinations).join('/')
}
@@ -935,29 +937,33 @@ export function DownloadDialog({ onOpenSupportedSites, onOpenSettings }: Downloa
{t('sites.viewAll')}
</Button>
</div>
{settings.oneClickDownload && (
<div className="flex items-center gap-2 text-xs text-primary">
<div className="w-1.5 h-1.5 rounded-full bg-primary" />
<span>{t('download.oneClickDownloadEnabled')}</span>
{onOpenSettings && (
<Button
type="button"
variant="ghost"
size="sm"
className="text-xs h-auto"
onClick={() => {
setOpen(false)
onOpenSettings()
}}
>
{t('download.goToSettings')}
</Button>
)}
</div>
)}
</div>
{/* One-click download indicator */}
{settings.oneClickDownload && (
<div className="w-full">
<div className="rounded-lg border bg-muted/30 p-2.5">
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>{t('download.oneClickDownloadEnabled')}</span>
{onOpenSettings && (
<Button
type="button"
variant="ghost"
size="sm"
className="text-xs h-auto"
onClick={() => {
setOpen(false)
onOpenSettings()
}}
>
{t('download.goToSettings')}
</Button>
)}
</div>
</div>
</div>
)}
{/* Error Display */}
{error && (
<div className="rounded-lg border border-destructive/20 bg-destructive/5 p-3">

View File

@@ -73,23 +73,31 @@ export function FormatSelector({
useEffect(() => {
// Filter and sort formats
// Exclude m3u8/HLS formats as they are streaming formats not suitable for direct download
const videos = formats.filter(
(f) =>
f.video_ext !== 'none' &&
f.vcodec &&
f.vcodec !== 'none' &&
f.protocol !== 'm3u8' &&
f.protocol !== 'm3u8_native'
const isVideoFormat = (format: VideoFormat) =>
format.video_ext !== 'none' && format.vcodec && format.vcodec !== 'none'
const isAudioFormat = (format: VideoFormat) =>
format.acodec &&
format.acodec !== 'none' &&
(format.video_ext === 'none' || !format.video_ext)
const isHlsFormat = (format: VideoFormat) =>
format.protocol === 'm3u8' || format.protocol === 'm3u8_native'
const videoCandidates = formats.filter(
(format) => isVideoFormat(format) && !isHlsFormat(format)
)
const audios = formats.filter(
(f) =>
f.acodec &&
f.acodec !== 'none' &&
(f.video_ext === 'none' || !f.video_ext) &&
f.protocol !== 'm3u8' &&
f.protocol !== 'm3u8_native'
const audioCandidates = formats.filter(
(format) => isAudioFormat(format) && !isHlsFormat(format)
)
const videos =
videoCandidates.length > 0
? videoCandidates
: formats.filter((format) => isVideoFormat(format))
const audios =
audioCandidates.length > 0
? audioCandidates
: formats.filter((format) => isAudioFormat(format))
// Apply showMoreFormats filter
const filteredVideos = settings.showMoreFormats
? videos
@@ -99,6 +107,9 @@ export function FormatSelector({
? audios
: audios.filter((f) => f.ext !== 'webm')
const finalVideos = filteredVideos.length > 0 ? filteredVideos : videos
const finalAudios = filteredAudios.length > 0 ? filteredAudios : audios
// Sort formats by quality (best first)
const sortVideoFormatsByQuality = (a: VideoFormat, b: VideoFormat) => {
// Sort by height (higher is better)
@@ -138,23 +149,23 @@ export function FormatSelector({
return 0
}
filteredVideos.sort(sortVideoFormatsByQuality)
filteredAudios.sort(sortAudioFormatsByQuality)
finalVideos.sort(sortVideoFormatsByQuality)
finalAudios.sort(sortAudioFormatsByQuality)
setVideoFormats(filteredVideos)
setAudioFormats(filteredAudios)
setVideoFormats(finalVideos)
setAudioFormats(finalAudios)
// Auto-select best format based on preferences
if (filteredVideos.length > 0 && !selectedVideo) {
const preferred = pickVideoFormatForPreset(filteredVideos, settings.oneClickQuality)
if (finalVideos.length > 0 && !selectedVideo) {
const preferred = pickVideoFormatForPreset(finalVideos, settings.oneClickQuality)
if (preferred) {
setSelectedVideo(preferred.format_id)
onVideoFormatChange?.(preferred.format_id)
}
}
if (filteredAudios.length > 0 && !selectedAudio) {
const best = filteredAudios[0]
if (finalAudios.length > 0 && !selectedAudio) {
const best = finalAudios[0]
setSelectedAudio(best.format_id)
onAudioFormatChange?.(best.format_id)
}