mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-09-27 20:11:15 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
730069a3c6 | ||
|
|
7a0cbd7456 | ||
|
|
5d676b45e7 | ||
|
|
a7f3d84a7d | ||
|
|
fbce5e5b7b | ||
|
|
cc7cc38392 | ||
|
|
6a9e5a063b | ||
|
|
cf8c95711c | ||
|
|
3683e4245f | ||
|
|
01affd8ddf | ||
|
|
faba3f516b | ||
|
|
c1d9448a2b | ||
|
|
1bff8376b4 | ||
|
|
3f44c01bd7 |
@@ -219,7 +219,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
|
||||
#### Linux
|
||||
```
|
||||
~/.config/Freedom Loader/config.json
|
||||
~/.config/Freedom\ Loader/config.json
|
||||
```
|
||||
|
||||
### Available Options
|
||||
@@ -233,6 +233,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"keepPlaylistOrder": false,
|
||||
"createPlaylistFolders": true,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
@@ -256,6 +257,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
| `addMetadata` | boolean | `true` | Add metadata tags to downloaded files |
|
||||
| `verboseLogs` | boolean | `false` | Enable detailed logging for debugging |
|
||||
| `autoDownloadPlaylist` | boolean | `true` | Automatically download entire playlists |
|
||||
| `keepPlaylistOrder` | boolean | `false` | Add index before title, usefull for album |
|
||||
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||
| `customCodec` | string | `"h264"` | Video codec for encoding (supported: h264, h265, vp9, av1) |
|
||||
| `logSystem` | boolean | `true` | Enable application logging |
|
||||
@@ -276,7 +278,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
- `theora` - Theora (legacy open codec)
|
||||
|
||||
> [!NOTE]
|
||||
> Configuration changes may require an application restart to take effect.
|
||||
> Some configuration changes may require an application restart to take effect.
|
||||
|
||||
## Project Structure
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ const FEATURE_WHITELIST = new Set([
|
||||
"addMetadata",
|
||||
"verboseLogs",
|
||||
"autoDownloadPlaylist",
|
||||
"keepPlaylistOrder",
|
||||
"customCodec",
|
||||
"theme",
|
||||
"createPlaylistFolders",
|
||||
|
||||
24
config.js
24
config.js
@@ -68,13 +68,30 @@ function loadFeatures() {
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads feature flags from disk.
|
||||
* Use this instead of the cached configFeatures snapshot
|
||||
* when you need the latest user settings at call time.
|
||||
*
|
||||
* @returns {Object} Fresh feature flags from config file
|
||||
*/
|
||||
function reloadFeatures() {
|
||||
try {
|
||||
const raw = fs.readFileSync(featuresPath, "utf-8");
|
||||
return JSON.parse(raw);
|
||||
} catch (err) {
|
||||
// Fallback to cached snapshot if file read fails
|
||||
return configFeatures;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory snapshot of application feature flags.
|
||||
*
|
||||
* Note: Changes to the config file are not automatically reflected.
|
||||
* A restart or reload is required.
|
||||
*/
|
||||
const configFeatures = loadFeatures();
|
||||
let configFeatures = loadFeatures();
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@@ -102,6 +119,11 @@ module.exports = {
|
||||
*/
|
||||
configFeatures,
|
||||
|
||||
/**
|
||||
* Function to reload the config
|
||||
*/
|
||||
reloadFeatures,
|
||||
|
||||
/**
|
||||
* Path to the active configuration file
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"keepPlaylistOrder": false,
|
||||
"createPlaylistFolders": true,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
|
||||
1488
package-lock.json
generated
1488
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "freedom-loader",
|
||||
"productName": "Freedom Loader",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.1",
|
||||
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
||||
"description": "Free and open-source GUI for yt-dlp — download video and audio from hundreds of platforms",
|
||||
"homepage": "https://masteracnolo.github.io/Freedom-Loader-Site/",
|
||||
@@ -22,9 +22,12 @@
|
||||
"build:linux": "electron-builder --linux",
|
||||
"build:rpm": "bash scripts/create-copr-package.sh",
|
||||
"build:all": "electron-builder -wl",
|
||||
"release": "bash scripts/release.sh",
|
||||
"release:publish": "bash scripts/release.sh --publish",
|
||||
"release:dry-run": "bash scripts/release.sh --dry-run",
|
||||
|
||||
"test": "jest",
|
||||
"test:unit": "jest tests/unit",
|
||||
"test:unit": "jest test/unit",
|
||||
|
||||
"update": "npm update"
|
||||
},
|
||||
@@ -44,7 +47,7 @@
|
||||
"@electron/devtron": "^2.1.1",
|
||||
"@jest/globals": "^30.4.1",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"electron": "^41.2.0",
|
||||
"electron": "^43.3.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"jest": "^30.4.2",
|
||||
"playwright": "^1.62.1"
|
||||
@@ -99,7 +102,7 @@
|
||||
"win": {
|
||||
"target": "nsis",
|
||||
"icon": "build/app-icon.ico",
|
||||
"artifactName": "${productName}-Setup-${version}.${ext}",
|
||||
"artifactName": "Freedom-Loader-Setup-${version}.${ext}",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "resources/binaries/win-32/yt-dlp.exe",
|
||||
|
||||
@@ -106,10 +106,10 @@
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="autoCheckInfo" id="autoCheckInfo">
|
||||
<input data-key="keepPlaylistOrder" id="keepPlaylistOrder" type="checkbox">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Fetch Info</span>
|
||||
<small>Automatically fetch information for new downloads.</small>
|
||||
<span class="setting-title">Keep Playlist Order</span>
|
||||
<small>Add index before output file, to keep the order.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -135,7 +135,15 @@
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-item">
|
||||
<input data-key="autoCheckInfo" id="autoCheckInfo" type="checkbox">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Auto Fetch Info</span>
|
||||
<small>Automatically fetch information for new downloads.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input type="checkbox" data-key="addMetadata" id="addMetadata">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">Add Metadata</span>
|
||||
|
||||
@@ -30,15 +30,15 @@ function formatSize(bytes) {
|
||||
*/
|
||||
async function fetchVideoInfo(url) {
|
||||
try {
|
||||
const res = await fetch("/info", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({ url }),
|
||||
const res = await fetch(`/info?url=${encodeURIComponent(url)}`, {
|
||||
method: "GET",
|
||||
headers: {"Accept": "application/json"}
|
||||
});
|
||||
|
||||
if (!res.ok) return { error: `An Error occured when fetching info` };
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) return {error: `An Error occured when fetching info`};
|
||||
|
||||
if (!data) return { error: "Data is Missing" };
|
||||
|
||||
return data;
|
||||
|
||||
@@ -6,15 +6,74 @@ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
||||
RPM_OUT="$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Resolve fpm from electron-builder cache
|
||||
# Excludes Ruby gem wrappers, targets standalone binaries only
|
||||
# Prefers fpm@2.x over legacy fpm-1.9.x
|
||||
# ------------------------------------------------------------
|
||||
FPM=$(find ~/.cache/electron-builder -name "fpm" -type f 2>/dev/null \
|
||||
| grep -v "/gems/bin/fpm" \
|
||||
| grep -v "/gems/gems/" \
|
||||
| grep -v "/lib/app/bin/fpm" \
|
||||
| grep "fpm@" \
|
||||
| head -1)
|
||||
|
||||
# Fallback to legacy fpm-1.9.x if fpm@2.x not found
|
||||
if [ -z "$FPM" ]; then
|
||||
FPM=$(find ~/.cache/electron-builder -name "fpm" -type f 2>/dev/null \
|
||||
| grep -v "/gems/" \
|
||||
| grep -v "/lib/app/bin/fpm" \
|
||||
| head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$FPM" ]; then
|
||||
echo "Error: fpm not found in electron-builder cache. Run 'npm run build:linux' once first to download it."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using fpm: $FPM"
|
||||
|
||||
# Check that linux-unpacked exists
|
||||
if [ ! -d "$ROOT_DIR/dist/linux-unpacked" ]; then
|
||||
echo "Error: dist/linux-unpacked not found. Run 'npm run build:linux' first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 1 — Build .rpm from linux-unpacked via fpm
|
||||
# ------------------------------------------------------------
|
||||
# Prepare staging files
|
||||
# fpm requires exact file-to-destination mappings to avoid
|
||||
# scanning system directories (which causes permission errors)
|
||||
# ------------------------------------------------------------
|
||||
|
||||
# Shell wrapper for /usr/bin/freedom-loader
|
||||
TMP_WRAPPER="$ROOT_DIR/.rpm-pkg-tmp-wrapper"
|
||||
mkdir -p "$TMP_WRAPPER"
|
||||
cat > "$TMP_WRAPPER/freedom-loader" << 'EOF'
|
||||
#!/bin/sh
|
||||
exec /opt/freedom-loader/freedom-loader "$@"
|
||||
EOF
|
||||
chmod +x "$TMP_WRAPPER/freedom-loader"
|
||||
|
||||
# Desktop entry file
|
||||
TMP_DESKTOP="$ROOT_DIR/.rpm-pkg-tmp-desktop"
|
||||
mkdir -p "$TMP_DESKTOP"
|
||||
cat > "$TMP_DESKTOP/freedom-loader.desktop" << 'EOF'
|
||||
[Desktop Entry]
|
||||
Name=Freedom Loader
|
||||
Exec=/opt/freedom-loader/freedom-loader %U
|
||||
Icon=freedom-loader
|
||||
Type=Application
|
||||
Categories=AudioVideo;Utility;Network;
|
||||
Comment=Free and open-source GUI for yt-dlp
|
||||
StartupWMClass=Freedom Loader
|
||||
EOF
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 1 - Build .rpm from linux-unpacked via fpm
|
||||
# ------------------------------------------------------------
|
||||
echo "Building RPM..."
|
||||
fpm -s dir -t rpm --force \
|
||||
"$FPM" -s dir -t rpm --force \
|
||||
--rpm-use-file-permissions \
|
||||
--name freedom-loader \
|
||||
--version "$VERSION" \
|
||||
--architecture x86_64 \
|
||||
@@ -26,14 +85,21 @@ fpm -s dir -t rpm --force \
|
||||
--depends nss \
|
||||
--depends libXScrnSaver \
|
||||
--depends at-spi2-core \
|
||||
--rpm-attr "4755,root,root:/opt/freedom-loader/chrome-sandbox" \
|
||||
--package "$RPM_OUT" \
|
||||
"$ROOT_DIR/dist/linux-unpacked/=/opt/freedom-loader" \
|
||||
"$TMP_WRAPPER/freedom-loader=/usr/bin/freedom-loader" \
|
||||
"$TMP_DESKTOP/freedom-loader.desktop=/usr/share/applications/freedom-loader.desktop" \
|
||||
"$ROOT_DIR/build/app-icon.png=/usr/share/icons/hicolor/512x512/apps/freedom-loader.png" \
|
||||
"$ROOT_DIR/package/com.masteracnolo.freedomloader.metainfo.xml=/usr/share/metainfo/com.masteracnolo.freedomloader.metainfo.xml"
|
||||
|
||||
rm -rf "$TMP_WRAPPER" "$TMP_DESKTOP"
|
||||
echo "RPM built: $RPM_OUT"
|
||||
|
||||
# Step 2 — Build SRPM for COPR (wraps the .rpm above)
|
||||
# ------------------------------------------------------------
|
||||
# Step 2 - Build SRPM for COPR
|
||||
# Wraps the .rpm above into a source RPM for COPR submission
|
||||
# ------------------------------------------------------------
|
||||
echo "Building SRPM for COPR..."
|
||||
mkdir -p "$ROOT_DIR/srpm-out"
|
||||
|
||||
|
||||
187
scripts/release.sh
Executable file
187
scripts/release.sh
Executable file
@@ -0,0 +1,187 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
||||
TAG="v$VERSION"
|
||||
|
||||
# Parse arguments
|
||||
PUBLISH_SNAP=false
|
||||
PUBLISH_COPR=false
|
||||
DRY_RUN=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--snap) PUBLISH_SNAP=true ;;
|
||||
--copr) PUBLISH_COPR=true ;;
|
||||
--publish) PUBLISH_SNAP=true; PUBLISH_COPR=true ;;
|
||||
--dry-run) DRY_RUN=true ;;
|
||||
*) echo "⚠️ Argument inconnu : $arg" ; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Freedom Loader - Release Pipeline"
|
||||
echo " Version : $VERSION"
|
||||
echo " Tag : $TAG"
|
||||
echo " Snap : $PUBLISH_SNAP"
|
||||
echo " COPR : $PUBLISH_COPR"
|
||||
echo " Dry run : $DRY_RUN"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 0.1 - Check Dependencies
|
||||
# ------------------------------------------------------------
|
||||
echo "[0/5] Checking dependencies..."
|
||||
|
||||
command -v gh >/dev/null 2>&1 || { echo "'gh' CLI missing, need to be installed (https://cli.github.com/)."; exit 1; }
|
||||
|
||||
if [ "$PUBLISH_SNAP" = true ] && [ "$DRY_RUN" = false ]; then
|
||||
command -v snapcraft >/dev/null 2>&1 || { echo "'snapcraft' missing, need to be installed."; exit 1; }
|
||||
fi
|
||||
|
||||
if [ "$PUBLISH_COPR" = true ] && [ "$DRY_RUN" = false ]; then
|
||||
command -v copr-cli >/dev/null 2>&1 || { echo "'copr-cli' missing, need to be installed."; exit 1; }
|
||||
fi
|
||||
|
||||
echo "All dependencies found"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 0.2 - Clean Workspace
|
||||
# ------------------------------------------------------------
|
||||
echo "[0/5] Cleaning workspace..."
|
||||
# On supprime les dossiers générés lors des builds précédents
|
||||
rm -rf "$ROOT_DIR/dist" "$ROOT_DIR/srpm-out"
|
||||
echo "Workspace cleaned (dist/ and srpm-out/ removed)"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 1 - Build Linux packages (AppImage, deb, snap)
|
||||
# ------------------------------------------------------------
|
||||
echo "[1/5] Building Linux packages..."
|
||||
cd "$ROOT_DIR"
|
||||
npm run build:linux
|
||||
echo "Linux packages built"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 2 - Build RPM binary + SRPM for COPR
|
||||
# Uses the create-copr-package.sh script which handles:
|
||||
# - fpm binary resolution from electron-builder cache
|
||||
# - .rpm generation from linux-unpacked
|
||||
# - .src.rpm generation for COPR submission
|
||||
# ------------------------------------------------------------
|
||||
echo "[2/5] Building RPM and SRPM..."
|
||||
bash "$ROOT_DIR/scripts/create-copr-package.sh"
|
||||
echo "RPM and SRPM built"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 3 - Build Windows installer
|
||||
# Note: requires Windows binaries to be present locally under
|
||||
# resources/binaries/win-32/ (not committed to the repo)
|
||||
# ------------------------------------------------------------
|
||||
echo "[3/5] Building Windows installer..."
|
||||
npm run build:win
|
||||
echo " Windows installer built"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 4 - Create a draft release on GitHub
|
||||
# Release notes are pre-filled with the standard template.
|
||||
# Edit and publish manually from the GitHub web interface.
|
||||
# Requires gh CLI to be installed and authenticated.
|
||||
# ------------------------------------------------------------
|
||||
echo "[4/5] Creating GitHub draft release $TAG..."
|
||||
|
||||
# Utilise une méthode un peu plus robuste pour trouver l'ancien tag (ignore le HEAD actuel s'il est déjà tagué)
|
||||
PREV_TAG=$(git rev-list --tags --skip=1 --max-count=1 | xargs git describe --tags 2>/dev/null || echo "previous")
|
||||
|
||||
RELEASE_NOTES="# Freedom Loader - $VERSION
|
||||
|
||||
## Found a bug or issue?
|
||||
Please report it in the [GitHub Issues](https://github.com/MasterAcnolo/Freedom-Loader/issues) section.
|
||||
|
||||
## Next Release (non-exhaustive roadmap)
|
||||
- More format options
|
||||
- Subtitle support
|
||||
- Improved UI / UX
|
||||
- Language selection
|
||||
- Download specific parts of videos
|
||||
- File renaming options
|
||||
- Parallel downloads
|
||||
- Skip sponsored parts automatically
|
||||
|
||||
**Full Changelog**: https://github.com/MasterAcnolo/Freedom-Loader/compare/${PREV_TAG}...${TAG}"
|
||||
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
gh release create "$TAG" \
|
||||
--title "$TAG" \
|
||||
--notes "$RELEASE_NOTES" \
|
||||
--draft \
|
||||
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe" \
|
||||
"$ROOT_DIR/dist/Freedom-Loader-Setup-${VERSION}.exe.blockmap" \
|
||||
"$ROOT_DIR/dist/freedom-loader-${VERSION}.x86_64.rpm" \
|
||||
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.deb" \
|
||||
"$ROOT_DIR/dist/Freedom Loader-${VERSION}.AppImage" \
|
||||
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \
|
||||
"$ROOT_DIR/dist/latest.yml" \
|
||||
"$ROOT_DIR/dist/latest-linux.yml"
|
||||
echo "Draft release created: https://github.com/MasterAcnolo/Freedom-Loader/releases"
|
||||
else
|
||||
echo " [DRY RUN] gh release create $TAG (skipped)"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Step 5 - Publish to package stores
|
||||
# Snap and COPR are opt-in via --snap, --copr, or --publish.
|
||||
# Without these flags, this step is skipped entirely.
|
||||
# Run after the GitHub release is published (not the draft),
|
||||
# so store users always get the same version as GitHub users.
|
||||
# ------------------------------------------------------------
|
||||
echo "[5/5] Publishing to package stores..."
|
||||
|
||||
if [ "$PUBLISH_SNAP" = true ]; then
|
||||
echo " Publishing to Snap Store..."
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
export PATH=$PATH:/var/lib/snapd/snap/bin
|
||||
snapcraft upload \
|
||||
"$ROOT_DIR/dist/freedom-loader_${VERSION}_amd64.snap" \
|
||||
--release=stable
|
||||
echo " Snap published"
|
||||
else
|
||||
echo " [DRY RUN] snapcraft upload (skipped)"
|
||||
fi
|
||||
else
|
||||
echo " Snap: skipped (use --snap or --publish to enable)"
|
||||
fi
|
||||
|
||||
if [ "$PUBLISH_COPR" = true ]; then
|
||||
echo " Publishing to COPR..."
|
||||
if [ "$DRY_RUN" = false ]; then
|
||||
copr-cli build freedom-loader \
|
||||
"$ROOT_DIR/srpm-out/freedom-loader-${VERSION}-1"*.src.rpm
|
||||
echo "COPR build triggered"
|
||||
else
|
||||
echo " [DRY RUN] copr-cli build (skipped)"
|
||||
fi
|
||||
else
|
||||
echo " COPR: skipped (use --copr or --publish to enable)"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Done - Summary
|
||||
# ------------------------------------------------------------
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Release pipeline complete."
|
||||
echo ""
|
||||
echo " Next steps:"
|
||||
echo " 1. Fill in the release notes on GitHub"
|
||||
echo " 2. Publish the draft release"
|
||||
if [ "$PUBLISH_SNAP" = false ] || [ "$PUBLISH_COPR" = false ]; then
|
||||
echo ""
|
||||
echo " Store publishing (not done yet):"
|
||||
[ "$PUBLISH_SNAP" = false ] && echo " - Snap : bash scripts/release.sh --snap"
|
||||
[ "$PUBLISH_COPR" = false ] && echo " - COPR : bash scripts/release.sh --copr"
|
||||
fi
|
||||
echo "============================================"
|
||||
@@ -18,7 +18,9 @@ const { isValidUrl } = require("../helpers/validation.helpers");
|
||||
*/
|
||||
async function infoController(req, res) {
|
||||
|
||||
const url = req.body.url || req.query.url; // Supports POST and GET requests
|
||||
// Previously, i support both POST and GET element, but it's kinda weird. So now it only support GET.
|
||||
const url = req.query.url;
|
||||
const encodedUrl = encodeURIComponent(url)
|
||||
|
||||
/**
|
||||
* Basic validation:
|
||||
@@ -30,9 +32,10 @@ async function infoController(req, res) {
|
||||
return res.status(400).send("Invalid URL Or Missing");
|
||||
}
|
||||
|
||||
logger.info(`Info request received. URL: ${url}`);
|
||||
logger.info(`Info request received. RAW URL: ${url}`);
|
||||
logger.info(`Info request received. ENCODED: ${encodedUrl}`);
|
||||
|
||||
// Lightweight heuristic to detect playlist URLs
|
||||
// Lightweight heuristic to detect playlist URLs. It works for Youtube.
|
||||
const isPlaylistUrl = url.includes("&list") || url.includes("?list");
|
||||
|
||||
logger.info(
|
||||
@@ -86,6 +89,16 @@ async function infoController(req, res) {
|
||||
*/
|
||||
if (data._type === "playlist") {
|
||||
|
||||
// If the playlist got only one video, show it as a video.
|
||||
if (data.entries && data.entries.length === 1) {
|
||||
|
||||
const singleVideo = parseVideo(data.entries[0]);
|
||||
|
||||
logger.info(`Playlist with 1 item converted to video: ${singleVideo.title}`);
|
||||
|
||||
return res.json(singleVideo);
|
||||
}
|
||||
|
||||
const playlist = parsePlaylist(data);
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -62,7 +62,7 @@ function validateCodec(codec){
|
||||
*
|
||||
* @returns {string[]} Array of arguments ready to be passed to yt-dlp.
|
||||
*/
|
||||
function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
function buildYtDlpArgs({url, audioOnly, quality, outputFolder, isPlaylist}) {
|
||||
|
||||
logger.info("--- CONFIGURATION ---");
|
||||
|
||||
@@ -70,6 +70,7 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
logger.info(`CONFIG discordRPC: ${configFeatures.discordRPC}`);
|
||||
logger.info(`CONFIG customTopBar: ${configFeatures.customTopBar}`);
|
||||
logger.info(`CONFIG autoCheckInfo: ${configFeatures.autoCheckInfo}`);
|
||||
logger.info(`CONFIG keepPlaylistOrder: ${configFeatures.keepPlaylistOrder}`);
|
||||
logger.info(`CONFIG addThumbnail: ${configFeatures.addThumbnail}`);
|
||||
logger.info(`CONFIG addMetadata: ${configFeatures.addMetadata}`);
|
||||
logger.info(`CONFIG verboseLogs: ${configFeatures.verboseLogs}`);
|
||||
@@ -117,7 +118,19 @@ function buildYtDlpArgs({ url, audioOnly, quality, outputFolder }) {
|
||||
};
|
||||
|
||||
args.push("-f", qualityMap[quality] || "best");
|
||||
args.push("-o", path.join(outputFolder, "%(title)s.%(ext)s"));
|
||||
|
||||
/**
|
||||
* Default title output template
|
||||
* @type {string}
|
||||
*/
|
||||
let outputTemplate = "%(title)s.%(ext)s";
|
||||
|
||||
// If it's a playlist, and we want to keep the playlist order. Add an index before the title.
|
||||
if (isPlaylist && configFeatures.keepPlaylistOrder && configFeatures.autoDownloadPlaylist) {
|
||||
outputTemplate = "%(playlist_index)02d - %(title)s.%(ext)s";
|
||||
}
|
||||
|
||||
args.push("-o", path.join(outputFolder, outputTemplate));
|
||||
args.push(url);
|
||||
|
||||
return args.filter(Boolean);
|
||||
|
||||
@@ -2,6 +2,12 @@ const { Notification, shell } = require("electron");
|
||||
const { iconPaths } = require("./path.helpers");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
/**
|
||||
* Set that is holding all notifications. Used to avoid Race Condition and notification crash
|
||||
* @type {Set<any>}
|
||||
*/
|
||||
const activeNotifications = new Set();
|
||||
|
||||
/**
|
||||
* Displays a system notification when a download completes successfully.
|
||||
*
|
||||
@@ -21,7 +27,27 @@ function notifyDownloadFinished(folder, notifyEnabled = true) {
|
||||
icon: iconPaths.confirm,
|
||||
});
|
||||
|
||||
notif.on("click", () => shell.openPath(folder));
|
||||
// Protect notification to garbage collection and race condition, by adding a 150ms timeout before trying to open the folder
|
||||
activeNotifications.add(notif);
|
||||
|
||||
notif.on("click", () => {
|
||||
activeNotifications.delete(notif);
|
||||
|
||||
setTimeout(() => {
|
||||
shell.openPath(folder).then((errorMessage) => {
|
||||
if (errorMessage) {
|
||||
logger.error(`Impossible d'ouvrir le dossier : ${errorMessage}`);
|
||||
}
|
||||
}).catch(err => {
|
||||
logger.error(`Erreur inattendue de shell.openPath : ${err}`);
|
||||
});
|
||||
}, 150);
|
||||
});
|
||||
|
||||
notif.on("close", () => {
|
||||
activeNotifications.delete(notif);
|
||||
});
|
||||
|
||||
notif.show();
|
||||
}
|
||||
|
||||
@@ -41,11 +67,16 @@ function notifyCookiesBrowserError(){
|
||||
icon: iconPaths.error,
|
||||
});
|
||||
|
||||
notif.on("click", () =>
|
||||
shell.openExternal(
|
||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||
)
|
||||
);
|
||||
activeNotifications.add(notif);
|
||||
|
||||
notif.on("click", () => {
|
||||
shell.openExternal("https://www.firefox.com/en-US/download/");
|
||||
activeNotifications.delete(notif);
|
||||
});
|
||||
|
||||
notif.on("close", () => {
|
||||
activeNotifications.delete(notif);
|
||||
});
|
||||
|
||||
notif.show();
|
||||
}
|
||||
@@ -65,11 +96,16 @@ function notifyFirefoxBrowserMissing() {
|
||||
icon: iconPaths.error,
|
||||
});
|
||||
|
||||
notif.on("click", () =>
|
||||
shell.openExternal(
|
||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||
)
|
||||
);
|
||||
activeNotifications.add(notif);
|
||||
|
||||
notif.on("click", () => {
|
||||
shell.openExternal("https://www.firefox.com/en-US/download/");
|
||||
activeNotifications.delete(notif);
|
||||
});
|
||||
|
||||
notif.on("close", () => {
|
||||
activeNotifications.delete(notif);
|
||||
});
|
||||
|
||||
notif.show();
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ const logger = createLogger({
|
||||
*/
|
||||
function logSessionStart(logDir, downloadPath) {
|
||||
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
||||
logger.info("============================================================")
|
||||
logger.info(`Application Version: ${config.version}`)
|
||||
logSystemInfo(logger, logDir, downloadPath)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ const express = require("express");
|
||||
const router = express.Router();
|
||||
const { infoController } = require("../controller/info.controller");
|
||||
|
||||
router.post("/", infoController);
|
||||
router.get("/", infoController);
|
||||
|
||||
module.exports = router;
|
||||
@@ -13,7 +13,9 @@ app.use(express.static(path.join(__dirname, "../public")));
|
||||
|
||||
// Routes
|
||||
app.use("/download", require("./routes/download.route"));
|
||||
app.use("/info", require("./routes/info.route"));
|
||||
app.use("/info", require("./routes/info.route"));
|
||||
|
||||
// Interface
|
||||
app.get("/", rateLimit, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ const { buildYtDlpArgs } = require("../helpers/buildArgs.helpers");
|
||||
const notify = require("../helpers/notify.helpers");
|
||||
const path = require("path");
|
||||
const { isSafePath } = require("../helpers/validation.helpers");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
const {reloadFeatures} = require("../../config");
|
||||
|
||||
/**
|
||||
* Reference to the currently running yt-dlp process.
|
||||
@@ -135,7 +135,10 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
||||
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
logger.info(`CONFIG createPlaylistFolders: ${configFeatures.createPlaylistFolders}`);
|
||||
|
||||
const userConfig = reloadFeatures();
|
||||
|
||||
logger.info(`CONFIG createPlaylistFolders: ${userConfig.createPlaylistFolders}`);
|
||||
|
||||
let outputFolder = options.outputFolder || defaultDownloadFolder;
|
||||
|
||||
@@ -159,7 +162,7 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
// Détecte si c'est une playlist et crée un dossier approprié
|
||||
const isPlaylist = options.playlistTitle || isPlaylistUrl(options.url);
|
||||
|
||||
if (isPlaylist && configFeatures.createPlaylistFolders) {
|
||||
if (isPlaylist && userConfig.createPlaylistFolders) {
|
||||
try {
|
||||
const playlistName = options.playlistTitle || "Untitled Playlist";
|
||||
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
|
||||
@@ -168,11 +171,16 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
logger.error(`Failed to create playlist folder: ${err.message}`);
|
||||
return reject(err);
|
||||
}
|
||||
} else if (isPlaylist && !configFeatures.createPlaylistFolders) {
|
||||
} else if (isPlaylist && !userConfig.createPlaylistFolders) {
|
||||
logger.info(`Playlist detected but createPlaylistFolders is disabled, using base folder`);
|
||||
}
|
||||
|
||||
const args = buildYtDlpArgs({ ...options, outputFolder: safeOutputFolder });
|
||||
const args = buildYtDlpArgs({
|
||||
...options,
|
||||
outputFolder: safeOutputFolder,
|
||||
isPlaylist
|
||||
});
|
||||
|
||||
logger.info(`[yt-dlp args] ${args.join(" ")}`);
|
||||
|
||||
const child = execFile(userYtDlp, args);
|
||||
|
||||
Reference in New Issue
Block a user