mirror of
https://github.com/MasterAcnolo/Freedom-Loader.git
synced 2026-09-28 04:21:15 +02:00
Compare commits
489 Commits
90d8a9f65d
...
1.6.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd43ae10fc | ||
|
|
6978277b4b | ||
|
|
b91c20d450 | ||
|
|
014b1ad52a | ||
|
|
ec93d20a39 | ||
|
|
09c511fcb8 | ||
|
|
f343fbd148 | ||
|
|
6d99d5f91c | ||
|
|
0f0bb874bf | ||
|
|
b9267ec408 | ||
|
|
311bff6df5 | ||
|
|
e4243a01ae | ||
|
|
52be9c6b4f | ||
|
|
e541eb442a | ||
|
|
e2c2dde4a2 | ||
|
|
ed4022dfd1 | ||
|
|
fcbc3f95ac | ||
|
|
f3293ef89a | ||
|
|
05e864b93a | ||
|
|
8ec901dc07 | ||
|
|
104500a82a | ||
|
|
3444f2fc0b | ||
|
|
86b595beff | ||
|
|
ed6efcf812 | ||
|
|
2c84ac7a70 | ||
|
|
3c649173a2 | ||
|
|
b4d4ff2665 | ||
|
|
c2584b6df3 | ||
|
|
ec16b5209d | ||
|
|
f2f1649493 | ||
|
|
e5076b0e0b | ||
|
|
4573917541 | ||
|
|
62c109939a | ||
|
|
b56ec1e62e | ||
|
|
1a68c58ee3 | ||
|
|
39c4a69be1 | ||
|
|
1993566040 | ||
|
|
2d19a35ef8 | ||
|
|
02c39d53b6 | ||
|
|
26059aa436 | ||
|
|
524cee1bac | ||
|
|
4de05fcd52 | ||
|
|
c2158d99cd | ||
|
|
d84e63fb56 | ||
|
|
12d77abbd3 | ||
|
|
398de8113d | ||
|
|
5b13f5d3e0 | ||
|
|
7d27889743 | ||
|
|
dd4c93e293 | ||
|
|
e0d426aa9f | ||
|
|
c51f3eb25e | ||
|
|
4f8600dbbc | ||
|
|
63d30720a8 | ||
|
|
acf7e67d0d | ||
|
|
1a1acb53d1 | ||
|
|
c153db0244 | ||
|
|
4fda512ab5 | ||
|
|
66471ce5ce | ||
|
|
027aaf7564 | ||
|
|
7f8d018bcb | ||
|
|
6c517255e3 | ||
|
|
dc7239379d | ||
|
|
225ccf4d2d | ||
|
|
ce2a0485ab | ||
|
|
1690cc18b2 | ||
|
|
a8c2cf4c81 | ||
|
|
f1962f7cd3 | ||
|
|
245a463d52 | ||
|
|
b404e1ba8c | ||
|
|
b7e54806fd | ||
|
|
4df373d52b | ||
|
|
73aa81673b | ||
|
|
df0952c231 | ||
|
|
c67a1511a5 | ||
|
|
d3058a66cf | ||
|
|
90939e968a | ||
|
|
ec83c20e16 | ||
|
|
3f3696e54e | ||
|
|
321309e294 | ||
|
|
b9334461bf | ||
|
|
e09f1a8913 | ||
|
|
bbb0b58d17 | ||
|
|
47f5b0efef | ||
|
|
6b7645804a | ||
|
|
9f85acbb59 | ||
|
|
32eb849808 | ||
|
|
bc1c62dc35 | ||
|
|
72c152d964 | ||
|
|
596a06a59d | ||
|
|
6d4692486e | ||
|
|
7e162debcc | ||
|
|
ae6b849aac | ||
|
|
bb8ddabb24 | ||
|
|
620ce7a40c | ||
|
|
9892c6a7dc | ||
|
|
bc6283ec5c | ||
|
|
b1377fe21e | ||
|
|
e62ef6bd0c | ||
|
|
49650ecb36 | ||
|
|
4a56af2491 | ||
|
|
3cc9f90fdd | ||
|
|
3b555d465e | ||
|
|
857ff773b0 | ||
|
|
5f2bba2c85 | ||
|
|
c9a5820076 | ||
|
|
336aa33485 | ||
|
|
76ce2a8019 | ||
|
|
5bdd13ad8e | ||
|
|
14b7d944d1 | ||
|
|
3835806e1a | ||
|
|
398e9acf66 | ||
|
|
2f203a0bc1 | ||
|
|
acce91bed8 | ||
|
|
6b33662f79 | ||
|
|
2805f5f0db | ||
|
|
7ae0ef9a99 | ||
|
|
83dc0a3f2f | ||
|
|
3179b52908 | ||
|
|
9564260c57 | ||
|
|
d7c2339f61 | ||
|
|
12a6709391 | ||
|
|
6b625326a4 | ||
|
|
8c40ac2b1c | ||
|
|
71081163a6 | ||
|
|
4ab6257b39 | ||
|
|
0d0d60c692 | ||
|
|
504149a220 | ||
|
|
a6ac2333c6 | ||
|
|
eb232de34e | ||
|
|
cda79bd28f | ||
|
|
78e0bcab37 | ||
|
|
dbe313fac4 | ||
|
|
2316b73adb | ||
|
|
62140be696 | ||
|
|
aa3cb24b20 | ||
|
|
cbb847800a | ||
|
|
8643ed2867 | ||
|
|
e712163c14 | ||
|
|
edcd7c8f70 | ||
|
|
e6b1c8ba62 | ||
|
|
d90ae6f236 | ||
|
|
9d4eeca7ee | ||
|
|
68c0f24ebb | ||
|
|
ef8fe131f6 | ||
|
|
596a563e06 | ||
|
|
f583a006f2 | ||
|
|
3e2b7e8190 | ||
|
|
4b725bed8b | ||
|
|
cf596ddb13 | ||
|
|
6812296dbc | ||
|
|
94003135fd | ||
|
|
95688a5dff | ||
|
|
592cc3314a | ||
|
|
67b058e1ac | ||
|
|
264ac071df | ||
|
|
0bd3e10094 | ||
|
|
c656b30172 | ||
|
|
3d1920e6e9 | ||
|
|
372fc921f0 | ||
|
|
c32e382685 | ||
|
|
4d77e843af | ||
|
|
3c30e49523 | ||
|
|
6b4310500e | ||
|
|
252618dfd1 | ||
|
|
fb7f0329b6 | ||
|
|
4feea9c530 | ||
|
|
e8fe990e01 | ||
|
|
086247068e | ||
|
|
48a218efca | ||
|
|
24fc685bf6 | ||
|
|
731cdc38ec | ||
|
|
391c343b6a | ||
|
|
cbe689e38e | ||
|
|
1ac035e160 | ||
|
|
fb8e2acda0 | ||
|
|
3dd9f53e80 | ||
|
|
269b9e69ba | ||
|
|
5f3b2a80a9 | ||
|
|
fdda78c802 | ||
|
|
a82e8ceefd | ||
|
|
c9dd09d341 | ||
|
|
78191d27ad | ||
|
|
33290e029b | ||
|
|
245cf01a43 | ||
|
|
2689424d01 | ||
|
|
bfb0873b3d | ||
|
|
3a11395301 | ||
|
|
5dc7c229b0 | ||
|
|
95497d252f | ||
|
|
784834cbbc | ||
|
|
b6d77444e1 | ||
|
|
dd4d770102 | ||
|
|
a374993ab1 | ||
|
|
d99fad1c3d | ||
|
|
9f4563dbd8 | ||
|
|
5bb6b6c165 | ||
|
|
f8e346b3cb | ||
|
|
ee685695de | ||
|
|
3e2ff74876 | ||
|
|
bbdb6a37fb | ||
|
|
7b55a13ce6 | ||
|
|
a4efedbe19 | ||
|
|
c3d43cafbf | ||
|
|
bf91df3a27 | ||
|
|
28928ec174 | ||
|
|
f5c51e254a | ||
|
|
36deddd823 | ||
|
|
f6eca7f3f6 | ||
|
|
ccf9e5bc03 | ||
|
|
082f19bda4 | ||
|
|
67ecd28aeb | ||
|
|
02d60bddd2 | ||
|
|
5236165b47 | ||
|
|
044091a9c3 | ||
|
|
a04730243d | ||
|
|
b9fa931cd7 | ||
|
|
c1c1f9a870 | ||
|
|
538cb6b51a | ||
|
|
5319bee9ee | ||
|
|
1edec6509e | ||
|
|
22a3eb7524 | ||
|
|
b5d058902c | ||
|
|
0e4134b172 | ||
|
|
02973067b1 | ||
|
|
b1064cceba | ||
|
|
536fb3678e | ||
|
|
dc3f19e95e | ||
|
|
a575416801 | ||
|
|
e74ef502d4 | ||
|
|
ee7c4a1f09 | ||
|
|
8771291e61 | ||
|
|
769753f25e | ||
|
|
455d51e7cd | ||
|
|
5ef9f42bd7 | ||
|
|
0c8322db13 | ||
|
|
e653a7f82f | ||
|
|
e5e9d90552 | ||
|
|
6a14056804 | ||
|
|
1dd9bd59b5 | ||
|
|
fb34eb9f80 | ||
|
|
ccbfccb76a | ||
|
|
416f3ac4c5 | ||
|
|
df1047c21e | ||
|
|
0a2fe5500c | ||
|
|
698d75184c | ||
|
|
2fa8951f67 | ||
|
|
a480cecf39 | ||
|
|
558408b12b | ||
|
|
95909e7ba4 | ||
|
|
c70218a966 | ||
|
|
de352039ea | ||
|
|
0169f61614 | ||
|
|
da2fd72984 | ||
|
|
1c43c795af | ||
|
|
e7fa3d9639 | ||
|
|
ecffb3eab1 | ||
|
|
e99b16eac6 | ||
|
|
d3478b18f7 | ||
|
|
42a235d916 | ||
|
|
f716316244 | ||
|
|
f059361b4e | ||
|
|
5b4cabdbd0 | ||
|
|
429720ae06 | ||
|
|
4f1f4c948d | ||
|
|
5855d7a933 | ||
|
|
4c85c138b8 | ||
|
|
9b10fab27b | ||
|
|
0933cae8e1 | ||
|
|
d16eed54ea | ||
|
|
83f7f843b0 | ||
|
|
a02264b8c9 | ||
|
|
2eda434a78 | ||
|
|
b8542b9c51 | ||
|
|
c8376464e5 | ||
|
|
d41006e5cb | ||
|
|
b3c13e59a7 | ||
|
|
d54120ba5a | ||
|
|
f1ce53a833 | ||
|
|
2321acfe7d | ||
|
|
c07ff64177 | ||
|
|
1ffc213fdd | ||
|
|
aba339116c | ||
|
|
0e3e65d36f | ||
|
|
d4aa1f3b94 | ||
|
|
3af1542ccd | ||
|
|
bfeb4da529 | ||
|
|
28f7459d9c | ||
|
|
9a59479b5c | ||
|
|
07387351da | ||
|
|
295971ef68 | ||
|
|
967583894f | ||
|
|
170bfc13f6 | ||
|
|
54a81b8e81 | ||
|
|
34343ef692 | ||
|
|
3152ae17dd | ||
|
|
750b07b69a | ||
|
|
118e4aa6a7 | ||
|
|
776105ce48 | ||
|
|
838f5481b6 | ||
|
|
2a66728f40 | ||
|
|
07899d0a43 | ||
|
|
2dd4f016a7 | ||
|
|
fdeca41ec7 | ||
|
|
9d2ddc33df | ||
|
|
c17ab8c934 | ||
|
|
12464f84dd | ||
|
|
b6583dc7ee | ||
|
|
9fadb4339e | ||
|
|
7c3fa0f69d | ||
|
|
c27da25a2b | ||
|
|
f36e456d1d | ||
|
|
0af4c6e0a3 | ||
|
|
4ee4053faf | ||
|
|
6f91a77692 | ||
|
|
937866c60c | ||
|
|
4cc2dbe3b7 | ||
|
|
20f887d07f | ||
|
|
4dbbdb94d5 | ||
|
|
3b01c1d560 | ||
|
|
f86aaaff46 | ||
|
|
778218aaca | ||
|
|
be44762483 | ||
|
|
689aa72e55 | ||
|
|
2d03c785ac | ||
|
|
37913b4edb | ||
|
|
399a50e64d | ||
|
|
50c93493a7 | ||
|
|
9206091cda | ||
|
|
db5e3ec9ec | ||
|
|
3a9d4ecb80 | ||
|
|
57a9634ba6 | ||
|
|
fea24e4cb8 | ||
|
|
e5d06b25f4 | ||
|
|
cd71d9ecbd | ||
|
|
fcc58e54d3 | ||
|
|
8208bab1b5 | ||
|
|
67e42d6e78 | ||
|
|
c5aefaef7f | ||
|
|
3d788285d7 | ||
|
|
dec4f721cf | ||
|
|
f0e91ef0a9 | ||
|
|
c53c9b9d43 | ||
|
|
7c8b37a35d | ||
|
|
17e5157d9a | ||
|
|
027572d666 | ||
|
|
2ed4d99c6f | ||
|
|
8fd3404064 | ||
|
|
fcbc3b3b77 | ||
|
|
7ea3e316ac | ||
|
|
cb20028119 | ||
|
|
9f5a63042d | ||
|
|
0708edeabc | ||
|
|
3190b7a835 | ||
|
|
922315883d | ||
|
|
2eff372377 | ||
|
|
ccf852fc97 | ||
|
|
9139fa4545 | ||
|
|
21eccfaae2 | ||
|
|
9e7e600645 | ||
|
|
4bf0e55d70 | ||
|
|
d3191b3c3e | ||
|
|
f91cdbefca | ||
|
|
3fdefad69b | ||
|
|
f910bf7543 | ||
|
|
cfc29956d0 | ||
|
|
737ed2b14e | ||
|
|
338f712bd4 | ||
|
|
0501496ee4 | ||
|
|
05d0ad77f8 | ||
|
|
6403cb3c71 | ||
|
|
3d7eadab21 | ||
|
|
4759040daf | ||
|
|
377f41e052 | ||
|
|
581e8bc3af | ||
|
|
5428e67451 | ||
|
|
fcad2e707a | ||
|
|
ca66d9f668 | ||
|
|
6e22a01e0f | ||
|
|
d99b5ae2b3 | ||
|
|
146ee1e17f | ||
|
|
567e1dfb04 | ||
|
|
6e047fb386 | ||
|
|
f4db1381da | ||
|
|
0f84bf275f | ||
|
|
01daca1d76 | ||
|
|
c8a13dc671 | ||
|
|
15163009f6 | ||
|
|
b3b3b99a68 | ||
|
|
5d7c592758 | ||
|
|
30d7bc683a | ||
|
|
150505b8bb | ||
|
|
874007b229 | ||
|
|
7e5facdf2a | ||
|
|
4a8422d002 | ||
|
|
4d4211d288 | ||
|
|
6a945eb413 | ||
|
|
7a0122d160 | ||
|
|
c06f0355f2 | ||
|
|
7bd4c7cf68 | ||
|
|
7abdc30cb0 | ||
|
|
6b8b5434f9 | ||
|
|
54031e7ead | ||
|
|
d70e6fead7 | ||
|
|
530c7f31e6 | ||
|
|
82ad264743 | ||
|
|
699696b4cf | ||
|
|
a3355341c1 | ||
|
|
a5e0c1d685 | ||
|
|
cdb4160411 | ||
|
|
ba2b51fddc | ||
|
|
180b8f2407 | ||
|
|
f8eca37f7e | ||
|
|
c9a3c0bda2 | ||
|
|
6787970343 | ||
|
|
7eda7bbf96 | ||
|
|
7c3771e8eb | ||
|
|
35f8b5fbdd | ||
|
|
4a3a893f41 | ||
|
|
9317f7b999 | ||
|
|
30ca987bf9 | ||
|
|
296bb3762b | ||
|
|
0f712e9c42 | ||
|
|
2b96ce0038 | ||
|
|
1e3239e0d2 | ||
|
|
11e5da54e0 | ||
|
|
a0cf89481b | ||
|
|
5512e7b3ec | ||
|
|
6219d07e35 | ||
|
|
2d611ac06c | ||
|
|
abc488b566 | ||
|
|
824077a5b0 | ||
|
|
a0401623fb | ||
|
|
34d577d22a | ||
|
|
4ac97a0221 | ||
|
|
b3d11bb07c | ||
|
|
03c374493c | ||
|
|
9aef213aa4 | ||
|
|
ab53ff6661 | ||
|
|
476e5362bd | ||
|
|
1686c872de | ||
|
|
80564f59ce | ||
|
|
5ed2b79781 | ||
|
|
0504890686 | ||
|
|
9a074e817d | ||
|
|
a86ecda352 | ||
|
|
5873fdff0f | ||
|
|
0ea5f7fb57 | ||
|
|
f2c18e427c | ||
|
|
d859d04abf | ||
|
|
62da2f862e | ||
|
|
4942c9109e | ||
|
|
fedaa47d0e | ||
|
|
35fc0c370b | ||
|
|
b6203f3546 | ||
|
|
9e5c2ffddf | ||
|
|
343aa99fea | ||
|
|
7dba6d8c6a | ||
|
|
4786bcee59 | ||
|
|
191fa7627a | ||
|
|
6606a5f9c1 | ||
|
|
a2871fc8e2 | ||
|
|
97fc95eb44 | ||
|
|
55a118f388 | ||
|
|
6294c4a764 | ||
|
|
8c15ce28bc | ||
|
|
521a0e41fe | ||
|
|
90b5c14bd1 | ||
|
|
4c5b963342 | ||
|
|
c42002d5db | ||
|
|
8e12ad742f | ||
|
|
c82dda321c | ||
|
|
05f6a892e0 | ||
|
|
2bd5369524 | ||
|
|
fdd0a2fa16 | ||
|
|
d413e47d58 | ||
|
|
26949ef6e9 | ||
|
|
1063494070 | ||
|
|
844b994eb8 | ||
|
|
72b9e45366 | ||
|
|
65cf3fad21 | ||
|
|
942e4032a0 | ||
|
|
f840d327f9 | ||
|
|
1da81e10d5 | ||
|
|
d46c92d1e0 | ||
|
|
bf5d1642ed | ||
|
|
e8c1c2a5b1 | ||
|
|
b3d39b5322 | ||
|
|
e7bbc109ba | ||
|
|
4ad514b064 |
3
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
3
.github/ISSUE_TEMPLATE/performance-issue.md
vendored
@@ -29,6 +29,9 @@ Clear description of the performance issue.
|
||||
- **Storage**: [e.g. SSD/HDD]
|
||||
- **Other Applications Opened**: [e.g. Discord/Opera GX]
|
||||
|
||||
> [!INFO]
|
||||
> At the start of the application, SystemInfo is printed in the logs, so you can just copy and paste it!
|
||||
|
||||
## Reproduction Details
|
||||
|
||||
- **Download URL**: [if applicable]
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/question.md
vendored
4
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -9,8 +9,8 @@ assignees: 'MasterAcnolo'
|
||||
|
||||
## Pre-submission Checklist
|
||||
|
||||
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/pages/faq)
|
||||
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/pages/wiki)
|
||||
- [ ] I have checked the [FAQ](https://masteracnolo.github.io/Freedom-Loader-Site/#/faq)
|
||||
- [ ] I have reviewed the [Wiki](https://masteracnolo.github.io/Freedom-Loader-Site/#/wiki)
|
||||
- [ ] I have searched existing issues
|
||||
|
||||
## Question
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Brief description of what this PR does.
|
||||
|
||||
## Related Issue
|
||||
## Related Issue (if applicable)
|
||||
|
||||
Fixes #(issue number)
|
||||
Closes #(issue number)
|
||||
@@ -30,7 +30,7 @@ Related to #(issue number)
|
||||
- [ ] I have tested with different download scenarios (video/audio, playlists)
|
||||
- [ ] I have checked the application logs for errors
|
||||
- [ ] I have verified the UI changes (if applicable)
|
||||
- [ ] I have tested on Windows 10 and/or Windows 11
|
||||
- [ ] I have tested on Windows 10 and/or Windows 11 or in any Linux Distribution
|
||||
|
||||
## Screenshots (if applicable)
|
||||
|
||||
|
||||
46
.github/workflows/codeql.yml
vendored
Normal file
46
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: "CodeQL PR"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
schedule:
|
||||
- cron: '00 00 * * 1'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
security-events: write
|
||||
packages: read
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
|
||||
- name: Run manual build steps
|
||||
if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'Manual build required'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
# Build Output Folder
|
||||
/dist
|
||||
/out
|
||||
/srpm-out
|
||||
|
||||
# Config JSON File. Create it and fill it if you want to have dev only settings
|
||||
config/config.dev.json
|
||||
@@ -16,8 +17,10 @@ config/config.dev.json
|
||||
ffmpeg
|
||||
ffprobe
|
||||
deno
|
||||
yt-dlp
|
||||
|
||||
# Windows Executable
|
||||
ffmpeg.exe
|
||||
ffprobe.exe
|
||||
deno.exe
|
||||
yt-dlp.exe
|
||||
@@ -94,3 +94,4 @@ chmod +x resources/binaries/linux/deno
|
||||
|
||||
- These binaries are excluded from version control via `.gitignore`.
|
||||
- Always use the latest stable release of each tool.
|
||||
- **Yt-dlp** binaries don't need to be updated manually. At every startup of the application, the binary tries to update itself
|
||||
@@ -6,6 +6,7 @@ Welcome! Thanks for your interest in Freedom Loader. Your contributions—bug re
|
||||
|
||||
## Quick Links
|
||||
|
||||
- [Development Guide](DEVELOPMENT.md)
|
||||
- [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
- [Issue Templates](.github/ISSUE_TEMPLATE/)
|
||||
- [Pull Request Templates](.github/PULL_REQUEST_TEMPLATE/)
|
||||
@@ -23,7 +24,7 @@ Found a bug or unexpected behavior? Open an [**issue**](https://github.com/Maste
|
||||
- Clear description of the bug
|
||||
- Steps to reproduce
|
||||
- App version and environment (Windows version, Firefox version if relevant)
|
||||
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log`
|
||||
- Logs from: `C:\Users\[USERNAME]\AppData\Local\FreedomLoader\logs\LOGS-YYYY-MM-DD.log` (Windows) or `~/.local/share/FreedomLoader/logs/` (Linux)
|
||||
- Screenshots if applicable
|
||||
|
||||
### 2. Request a Feature
|
||||
@@ -43,13 +44,11 @@ Have an idea to improve Freedom Loader? Open a [**Feature Request**](https://git
|
||||
- Follow the project's code style and conventions
|
||||
|
||||
**Process:**
|
||||
1. Fork the repository
|
||||
2. Create a feature branch: `git checkout -b feature/your-feature-name`
|
||||
3. Make your changes
|
||||
4. Test thoroughly on Windows 10/11
|
||||
5. Commit with clear messages: `git commit -m "Add feature X"`
|
||||
6. Push to your fork: `git push origin feature/your-feature-name`
|
||||
7. Open a Pull Request using the appropriate template
|
||||
1. Fork the repository.
|
||||
2. Check out our [**Developer Guide (DEVELOPMENT.md)**](./DEVELOPMENT.md) to set up your environment.
|
||||
3. Create a feature branch following our branching conventions.
|
||||
4. Make your changes and test thoroughly on Windows and/or Linux.
|
||||
5. Open a Pull Request using the appropriate template.
|
||||
|
||||
**Code Guidelines:**
|
||||
- Use camelCase for variables and functions
|
||||
@@ -72,41 +71,9 @@ Small contributions matter—don't hesitate to submit documentation PRs.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 16.x or higher
|
||||
- npm or yarn
|
||||
- Git
|
||||
- Windows 10/11 (for testing)
|
||||
Want to write some code? Awesome!
|
||||
|
||||
### Setup
|
||||
```bash
|
||||
# Clone your fork
|
||||
git clone https://github.com/YOUR-USERNAME/Freedom-Loader.git
|
||||
cd Freedom-Loader
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
```
|
||||
Freedom-Loader/
|
||||
├── main.js # Electron main process
|
||||
├── preload.js # Electron preload script
|
||||
├── config.js # Global configuration
|
||||
├── server/ # Express backend
|
||||
│ ├── routes/ # API routes
|
||||
│ ├── controller/ # Business logic
|
||||
│ └── helpers/ # Utility functions
|
||||
├── public/ # Frontend (HTML, CSS, JS)
|
||||
└── resources/ # Binaries (yt-dlp, ffmpeg, etc.)
|
||||
```
|
||||
Please read our [**Developer Guide (DEVELOPMENT.md)**](./DEVELOPMENT.md) for instructions on how to set up your local environment, our project architecture, and our Git branching rules.
|
||||
|
||||
---
|
||||
|
||||
@@ -114,12 +81,9 @@ Freedom-Loader/
|
||||
|
||||
Before submitting a PR, verify:
|
||||
- [ ] Download functionality works (video/audio)
|
||||
- [ ] Playlist downloads work
|
||||
- [ ] Settings panel functions correctly
|
||||
- [ ] Custom path selection works
|
||||
- [ ] No errors in application logs
|
||||
- [ ] UI changes work on all themes (if applicable)
|
||||
- [ ] Tested on Windows 10 and/or Windows 11
|
||||
- [ ] Any features wasn't break during the development
|
||||
- [ ] Tested on Windows 10/11 and/or any Linux Distribution
|
||||
|
||||
---
|
||||
|
||||
|
||||
388
DEVELOPMENT.md
Normal file
388
DEVELOPMENT.md
Normal file
@@ -0,0 +1,388 @@
|
||||
# Freedom Loader - Development Guide
|
||||
|
||||
Welcome to the Developer Guide! This document explains how to set up your local environment, project architecture, Git workflow, and the release process.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you begin, ensure you have the following installed on your machine:
|
||||
|
||||
- **Node.js** (v22 or higher recommended)
|
||||
- **npm** (comes with Node.js)
|
||||
- **Git**
|
||||
- **yt-dlp binaries**: Freedom Loader requires native binaries to function. Read [BINARIES.md](./BINARIES.md) for download instructions and placement in `resources/binaries/`.
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
### 1. Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/MasterAcnolo/Freedom-Loader.git
|
||||
cd Freedom-Loader
|
||||
```
|
||||
|
||||
### 2. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Install the Binaries
|
||||
|
||||
Follow the instructions in `BINARIES.md` to place `yt-dlp`, `ffmpeg`, `ffprobe`, and `deno` in the correct `resources/binaries/` folder for your OS.
|
||||
|
||||
### 4. Run in Development Mode
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
**Note**: Development mode automatically enables detailed colored logs and the DevTron extension for debugging.
|
||||
|
||||
---
|
||||
|
||||
## Available Scripts
|
||||
|
||||
### Development
|
||||
|
||||
- `npm start` — Start the app in development mode (no warnings)
|
||||
- `npm run start:warn` — Start with deprecation warnings visible
|
||||
- `npm run start:debug` — Start with full debug logging (Wayland, Electron logging, stack dumps)
|
||||
- `npm run dev` — Start with auto-reload on file changes (watches server/, app/, main.js, config.js)
|
||||
- `npm run dev:warn` — Dev mode with warnings
|
||||
- `npm run dev:debug` — Dev mode with full debug output and auto-reload
|
||||
|
||||
### Building
|
||||
|
||||
- `npm run build:linux` — Build Linux packages (AppImage, deb, snap)
|
||||
- `npm run build:win` — Build Windows installer (.exe)
|
||||
- `npm run build:rpm` — Build RPM and SRPM for COPR (Fedora)
|
||||
- `npm run build:all` — Build both Windows and Linux (not recommended because it adds both OS binaries into a single package. The output file is 2x bigger than expected).
|
||||
|
||||
### Testing
|
||||
|
||||
- `npm test` — Run all tests (unit + integration)
|
||||
- `npm run test:unit` — Run unit tests only
|
||||
|
||||
### Release Pipeline
|
||||
|
||||
- `npm run release` — Build all packages and create a draft GitHub release (no store publishing)
|
||||
- `npm run release:publish` — Build all packages and publish to Snap Store + COPR
|
||||
- `npm run release:dry-run` — Simulate the full release pipeline without publishing anything
|
||||
|
||||
### Maintenance
|
||||
|
||||
- `npm update` — Update npm dependencies
|
||||
|
||||
---
|
||||
|
||||
## Project Architecture
|
||||
|
||||
Freedom Loader is built with **Electron** (frontend) and **Node.js + Express** (backend), using a modular structure:
|
||||
|
||||
---
|
||||
|
||||
## Git & Branching Workflow
|
||||
|
||||
I use **Trunk-Based Development** (GitHub Flow):
|
||||
|
||||
### Core Principles
|
||||
|
||||
- **`main` is always deployable**: The `main` branch contains the latest stable code and must never be broken.
|
||||
- **No long-lived branches**: I no longer maintain version branches like `v1.6` or `v1.7`.
|
||||
- **Pull Requests for review**: All code changes go through PR review before merging to `main`.
|
||||
|
||||
### How to Contribute
|
||||
|
||||
#### 1. Create a feature branch
|
||||
|
||||
```bash
|
||||
git checkout -b feat/add-new-button
|
||||
# or for fixes:
|
||||
git checkout -b fix/ui-bug
|
||||
```
|
||||
|
||||
**Branch naming convention**:
|
||||
- `feat/` — New features
|
||||
- `fix/` — Bug fixes
|
||||
- `refactor/` — Code cleanup (no behavior change)
|
||||
- `docs/` — Documentation updates
|
||||
- `chore/` — Dependency updates, build config, etc.
|
||||
|
||||
#### 2. Make commits
|
||||
|
||||
```bash
|
||||
# Make changes
|
||||
git add .
|
||||
git commit -m "Brief, imperative description of the change"
|
||||
```
|
||||
|
||||
**Commit message tips**:
|
||||
- Use imperative mood: "Add theme caching" not "Added theme caching"
|
||||
- Keep commits logical and focused (one feature per commit if possible)
|
||||
- Link to issues if relevant: "Fix crash on download (fixes #42)"
|
||||
|
||||
#### 3. Push and open a PR
|
||||
|
||||
```bash
|
||||
git push origin feat/add-new-button
|
||||
```
|
||||
|
||||
Then open a PR on GitHub against `main`.
|
||||
|
||||
#### 4. Review and merge
|
||||
|
||||
- Address feedback in new commits (don't rebase — easier to review)
|
||||
- Once approved, merge via GitHub (use "Squash and merge" for clean history if many small commits and you find it relevant)
|
||||
|
||||
---
|
||||
|
||||
## Release Process
|
||||
|
||||
**Note**: Only project maintainers release to production. This section documents the process for transparency and for future maintainers.
|
||||
|
||||
Releases are fully automated via the `release.sh` script. The process handles building, packaging, and publishing to **all distribution channels**:
|
||||
- **GitHub Releases** (Windows .exe, Linux packages)
|
||||
- **Fedora COPR** (automatic RPM builds for Fedora 43+)
|
||||
- **Snap Store** (universal Linux)
|
||||
|
||||
### Before You Release
|
||||
|
||||
1. **Ensure `main` is green**: All tests pass, features are stable.
|
||||
2. **Update `package.json` version**:
|
||||
|
||||
```json
|
||||
"version": "1.6.1"
|
||||
```
|
||||
3. **Commit the version bump**: `git add package.json && git commit -m "chore: v1.6.1"`
|
||||
4. **Push to `main`**: `git push origin main`
|
||||
|
||||
### Step 1: Local Build & Draft Release
|
||||
|
||||
```bash
|
||||
npm run release
|
||||
```
|
||||
|
||||
This:
|
||||
1. Builds Linux packages (AppImage, deb, snap)
|
||||
2. Builds RPM and SRPM for COPR
|
||||
3. Builds Windows installer
|
||||
4. Creates a draft GitHub release
|
||||
|
||||
### Step 2: Edit & Publish on GitHub
|
||||
|
||||
1. Go to https://github.com/MasterAcnolo/Freedom-Loader/releases
|
||||
2. Edit the draft release — fill in changelog
|
||||
3. Click **"Publish"** to make it live
|
||||
|
||||
### Step 3: Publish to Distribution Channels
|
||||
|
||||
```bash
|
||||
npm run release:publish
|
||||
```
|
||||
|
||||
This automatically:
|
||||
- **Uploads to Snap Store**: Makes the app available via `snap install freedom-loader`
|
||||
- **Submits to Fedora COPR**: Builds and publishes RPMs for Fedora 43+ (users can `dnf install freedom-loader` from the COPR repo)
|
||||
|
||||
Both happen in parallel — users across all platforms get the release simultaneously.
|
||||
|
||||
---
|
||||
|
||||
## Linux Development Considerations
|
||||
|
||||
### Cross-Distribution Compatibility
|
||||
|
||||
Freedom Loader targets **Debian-based** (Ubuntu, Debian) and **Fedora-based** (Fedora, RHEL, openSUSE) distributions. When developing features, keep this in mind:
|
||||
|
||||
#### What to test
|
||||
|
||||
- **Feature works on Fedora 44+** (primary Linux development environment)
|
||||
- **Feature works on Ubuntu/Debian** (via deb package or AppImage)
|
||||
- **Feature doesn't break Snap confinement** (Snap has restricted filesystem/IPC access)
|
||||
- **Feature gracefully degrades on missing system dependencies**
|
||||
|
||||
#### Desktop Environment (DE) Compatibility
|
||||
|
||||
Test on at least **GNOME** and **KDE** (the most common DEs). Common pain points:
|
||||
|
||||
- **Themes**: May render differently on KDE vs GNOME — test both if possible
|
||||
- **File dialogs**: Some DEs use native file pickers, others fall back to Electron's
|
||||
- **Notifications**: System notification APIs vary (D-Bus, libnotify)
|
||||
- **Tray icons**: May not work identically across DEs
|
||||
|
||||
#### How to check locally
|
||||
|
||||
```bash
|
||||
# Test on Fedora (if available)
|
||||
npm run build:linux
|
||||
sudo dnf install dist/freedom-loader-*.x86_64.rpm
|
||||
freedom-loader
|
||||
|
||||
# Test AppImage (works on any distro)
|
||||
chmod +x dist/Freedom\ Loader-*.AppImage
|
||||
./dist/Freedom\ Loader-*.AppImage
|
||||
```
|
||||
|
||||
### CI/CD Gap
|
||||
|
||||
**Currently**: No automated testing across distros or DEs. Releases rely on manual testing before publish.
|
||||
|
||||
**Future improvement**: Automated tests via GitHub Actions (Ubuntu) + local testing on Fedora would catch cross-distro issues early. This is planned but not yet implemented.
|
||||
|
||||
For now, if you fix a Linux-specific bug or add a DE-dependent feature, **please mention it in your PR description** so reviewers can test extra carefully.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Test individual functions in isolation (no Electron required).
|
||||
|
||||
```bash
|
||||
npm run test:unit
|
||||
```
|
||||
|
||||
Covered:
|
||||
|
||||
(Tests will come soon)
|
||||
|
||||
### End-to-End Tests (Playwright)
|
||||
|
||||
Test the actual Electron app launching and basic UI interactions.
|
||||
|
||||
```bash
|
||||
npm run test
|
||||
```
|
||||
|
||||
Covered:
|
||||
|
||||
(Tests will come soon)
|
||||
|
||||
|
||||
### Running All Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Or include tests before release:
|
||||
|
||||
```bash
|
||||
npm test && npm run release
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Add a new feature
|
||||
|
||||
1. Create a feature branch: `git checkout -b feat/my-feature`
|
||||
2. Make changes, commit: `git add . && git commit -m "feat: my feature"`
|
||||
3. Push: `git push origin feat/my-feature`
|
||||
4. Open a PR on GitHub
|
||||
5. Once approved, merge to `main`
|
||||
6. (Later) Cut a release when ready: `npm run release`
|
||||
|
||||
### Fix a bug
|
||||
|
||||
Same as above, but use `fix/bug-name` branch and `git commit -m "fix: description"`.
|
||||
|
||||
### Test the app before releasing
|
||||
|
||||
```bash
|
||||
npm run dev # Auto-reload on file changes
|
||||
# Make changes, test in the UI
|
||||
npm test # Run all tests
|
||||
npm run release:dry-run # Simulate release (builds but doesn't publish)
|
||||
```
|
||||
|
||||
### Update dependencies
|
||||
|
||||
```bash
|
||||
npm update
|
||||
git add package.json package-lock.json
|
||||
git commit -m "chore: update dependencies"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### Rebuild only Linux (after quick fixes)
|
||||
|
||||
```bash
|
||||
npm run build:linux
|
||||
# Or just the RPM:
|
||||
npm run build:rpm
|
||||
```
|
||||
|
||||
### Publish to stores manually
|
||||
|
||||
If `npm run release:publish` fails partway:
|
||||
|
||||
```bash
|
||||
# Just Snap:
|
||||
snapcraft upload dist/freedom-loader_*.snap --release=stable
|
||||
|
||||
# Just COPR:
|
||||
copr-cli build freedom-loader srpm-out/*.src.rpm
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### App won't start in dev mode
|
||||
|
||||
```bash
|
||||
npm run start:debug
|
||||
```
|
||||
|
||||
Check the debug output for errors. Most common:
|
||||
- Missing binaries in `resources/binaries/`
|
||||
- Port 8787 already in use
|
||||
- A dead instance is still running — open Task Manager and kill the "Freedom Loader" process
|
||||
|
||||
### (Linux) Binaries in the right place but app still won't launch
|
||||
|
||||
You probably forgot to make them executable:
|
||||
|
||||
```bash
|
||||
chmod +x resources/binaries/linux/*
|
||||
```
|
||||
|
||||
The app will fail silently if binaries lack execute permissions.
|
||||
|
||||
### Build fails with "permission denied"
|
||||
|
||||
Make sure you have write access to `dist/` and `srpm-out/`:
|
||||
|
||||
```bash
|
||||
chmod -R u+w dist srpm-out
|
||||
npm run build:linux
|
||||
```
|
||||
|
||||
### Tests fail
|
||||
|
||||
Check for:
|
||||
- Missing test files in `tests/unit/`
|
||||
- Node modules out of sync: `rm -rf node_modules && npm install`
|
||||
|
||||
### GitHub release publish fails
|
||||
|
||||
Verify `gh` CLI is installed and authenticated:
|
||||
|
||||
```bash
|
||||
gh auth login
|
||||
gh release list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Questions?
|
||||
|
||||
For more info:
|
||||
- **Electron docs**: https://www.electronjs.org/docs
|
||||
- **electron-builder**: https://www.electron.build/
|
||||
- **This repo**: https://github.com/MasterAcnolo/Freedom-Loader
|
||||
|
||||
Happy coding! 🎉
|
||||
221
README.md
221
README.md
@@ -7,7 +7,7 @@
|
||||
|
||||
### **A clean, open-source multimedia downloader for Windows and Linux**
|
||||
|
||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://masteracnolo.github.io/Freedom-Loader-Site/)
|
||||
[](https://masteracnolo.github.io/Freedom-Loader-Workshop/)
|
||||
@@ -15,8 +15,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Freedom Loader is a desktop application built with Electron that provides a straightforward way to download video and audio content with full metadata support. The application focuses on reliability, transparency, and user privacy-no ads, no tracking, no questionable third-party services.
|
||||
@@ -25,7 +23,7 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
|
||||
### See related repo:
|
||||
|
||||
- **[Website](https://masteracnolo.github.io/Freedom-Loader-Site/)** - Official project website and documentation
|
||||
- **[Website](https://github.com/MasterAcnolo/Freedom-Loader-Site/)** - Official project website and documentation
|
||||
- **[Workshop](https://github.com/MasterAcnolo/Freedom-Loader-Workshop)** - Theme creation tool and theme browser
|
||||
|
||||
## Table of Contents
|
||||
@@ -35,14 +33,12 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
- [Usage](#usage)
|
||||
- [Preview](#preview)
|
||||
- [Configuration](#configuration)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Theme Workshop](#Theme-Workshop)
|
||||
- [Technology Stack](#technology-stack)
|
||||
- [Development](#development)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Contributing](#contributing)
|
||||
- [Submitting a community theme](#Submitting-a-community-theme)
|
||||
- [Support](#support)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
@@ -61,9 +57,9 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
### User Interface
|
||||
|
||||
- **Custom window controls** - Frameless window with custom top bar (optional)
|
||||
- **Custom themes** — ZIP-based theme system with live preview
|
||||
- **Theme Workshop** — web-based theme creator and browser
|
||||
- **Toast notifications** — in-app feedback system
|
||||
- **Custom themes** - ZIP-based theme system with live preview
|
||||
- **Theme Workshop** - web-based theme creator and browser
|
||||
- **Toast notifications** - in-app feedback system
|
||||
- **Real-time progress tracking** - Live download progress bar with network speed indicator
|
||||
- **Server-Sent Events (SSE)** - Non-blocking progress updates via event streaming
|
||||
- **Settings panel** - In-app configuration interface with live updates
|
||||
@@ -80,16 +76,17 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
- **Rate limiting** - Express-based request throttling to prevent server overload
|
||||
- **Graceful shutdown** - Proper cleanup of servers, logs, and RPC connections
|
||||
- **System notifications** - Windows notifications on download completion (clickable to open folder)
|
||||
- **Splash screen** — animated loading screen on startup
|
||||
- **Stop download** — ability to cancel in-progress downloads
|
||||
- **Playlist folders** — dedicated folder creation per playlist
|
||||
- **Splash screen** - animated loading screen on startup
|
||||
- **Stop download** - ability to cancel in-progress downloads
|
||||
- **Playlist folders** - dedicated folder creation per playlist
|
||||
- **Keep Playlist Order** - Add an Index before the title, could be useful for an album for example.
|
||||
- **System Tray** - Could minimize the application in the system Tray, allow it to run in the background.
|
||||
|
||||
## Installation
|
||||
|
||||
[](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
|
||||
[//]: # ([](https://snapcraft.io/freedom-loader))
|
||||
[](https://snapcraft.io/freedom-loader)
|
||||
[](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
|
||||
### Requirements
|
||||
@@ -106,7 +103,7 @@ The primary goal is to make media downloading accessible to users who want offli
|
||||
|
||||
### Linux
|
||||
|
||||
#### Fedora (via Copr — recommended)
|
||||
#### Fedora (via Copr - recommended)
|
||||
|
||||
Installing via Copr lets `dnf` handle future updates automatically:
|
||||
|
||||
@@ -122,18 +119,16 @@ sudo dnf install freedom-loader
|
||||
sudo apt install ./freedom-loader_1.x.x_amd64.deb
|
||||
```
|
||||
|
||||
[//]: # (#### Snap (any distro with snapd))
|
||||
#### Snap (any distro with snapd)
|
||||
|
||||
[//]: # ()
|
||||
[//]: # (```bash)
|
||||
|
||||
[//]: # (sudo snap install freedom-loader)
|
||||
|
||||
[//]: # (```)
|
||||
```bash
|
||||
sudo snap install freedom-loader
|
||||
```
|
||||
|
||||
#### RPM (Fedora/RHEL/openSUSE, without Copr)
|
||||
|
||||
```bash
|
||||
# Download the latest .rpm from the Releases page, then:
|
||||
sudo dnf install ./freedom-loader-1.x.y.x86_64.rpm
|
||||
```
|
||||
|
||||
@@ -162,7 +157,8 @@ freedom-loader
|
||||
|
||||
## Usage
|
||||
|
||||
> [!TIP] On Linux, you can start Freedom Loader using `freedom-loader` command
|
||||
> [!TIP]
|
||||
> On Linux, you can start Freedom Loader using `freedom-loader` command
|
||||
|
||||
### Basic Download
|
||||
|
||||
@@ -180,7 +176,8 @@ The application supports various configuration options:
|
||||
- Custom download directory selection
|
||||
- Discord Rich Presence activation
|
||||
- Theme customization
|
||||
- Log level configuration
|
||||
- Log level
|
||||
- And somes to see in the list [Configuration Options](#configuration-options) available
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
@@ -221,13 +218,14 @@ 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
|
||||
```json
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"systemTray": true,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
@@ -235,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,
|
||||
@@ -251,6 +250,7 @@ Freedom Loader can be configured either through the settings panel in the UI or
|
||||
| Option | Type | Default | Description |
|
||||
|------------------------------|---------|----------|------------------------------------------------------------|
|
||||
| `autoUpdate` | boolean | `true` | Enable automatic application updates |
|
||||
| `systemTray` | boolean | `false` | Allow app to minimize on close [EXPERIMENTAL] |
|
||||
| `discordRPC` | boolean | `true` | Enable Discord Rich Presence integration |
|
||||
| `customTopBar` | boolean | `true` | Use custom application top bar |
|
||||
| `autoCheckInfo` | boolean | `true` | Automatically fetch video information on URL paste |
|
||||
@@ -258,7 +258,8 @@ 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 |
|
||||
| `createPlaylistFolders` | boolean | `true` | Automatically create a folder for a playlist |
|
||||
| `keepPlaylistOrder` | boolean | `false` | Add index before title, usefull for album |
|
||||
| `createPlaylistFolders` | boolean | `false` | 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 |
|
||||
| `outputTitleCheck` | boolean | `true` | Verify output file titles |
|
||||
@@ -278,95 +279,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.
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
Freedom-Loader/
|
||||
├── app/ # Electron main process modules
|
||||
│ ├── autoUpdater.js
|
||||
│ ├── dependencyCheck.js
|
||||
│ ├── discordRPC.js
|
||||
│ ├── ipcHandlers.js
|
||||
│ ├── pathValidator.js
|
||||
│ ├── splashManager.js
|
||||
│ ├── themeManager.js
|
||||
│ ├── windowManager.js
|
||||
│ └── ytDlpUpdater.js
|
||||
├── build/ # Build resources and assets
|
||||
├── config/ # Configuration files
|
||||
│ ├── config.default.json
|
||||
│ └── config.dev.json
|
||||
├── public/ # Frontend assets and UI
|
||||
│ ├── index.html
|
||||
│ ├── splash.html
|
||||
│ ├── assets/
|
||||
│ │ ├── icon/ # Application icons
|
||||
│ │ └── logo/ # Logo assets
|
||||
│ ├── script/ # Frontend JavaScript modules
|
||||
│ │ ├── appVersion.js
|
||||
│ │ ├── clipboardPaste.js
|
||||
│ │ ├── custompath.js
|
||||
│ │ ├── customthemes.js
|
||||
│ │ ├── downloadstatus.js
|
||||
│ │ ├── fetchinfo.js
|
||||
│ │ ├── progressBar.js
|
||||
│ │ ├── settingsPanel.js
|
||||
│ │ ├── toast.js
|
||||
│ │ └── topbar.js
|
||||
│ └── styles/ # CSS stylesheets
|
||||
│ ├── styles.css
|
||||
│ ├── variables.css
|
||||
│ ├── components/ # Component-specific styles
|
||||
│ │ ├── checkbox.css
|
||||
│ │ ├── editpathbutton.css
|
||||
│ │ ├── loader.css
|
||||
│ │ ├── progressBar.css
|
||||
│ │ └── toast.css
|
||||
│ └── layout/ # Layout styles
|
||||
│ ├── container.css
|
||||
│ ├── form.css
|
||||
│ ├── header.css
|
||||
│ ├── settingsPanel.css
|
||||
│ ├── topbar.css
|
||||
│ └── videoinfo.css
|
||||
├── resources/ # Internal resources (binaries)
|
||||
├── server/ # Express backend server
|
||||
│ ├── logger.js
|
||||
│ ├── server.js
|
||||
│ ├── controller/ # Request handlers
|
||||
│ │ ├── download.controller.js
|
||||
│ │ └── info.controller.js
|
||||
│ ├── helpers/ # Utility functions
|
||||
│ │ ├── buildArgs.helpers.js
|
||||
│ │ ├── getBrowser.helpers.js
|
||||
│ │ ├── notify.helpers.js
|
||||
│ │ ├── parseInfo.helpers.js
|
||||
│ │ ├── path.helpers.js
|
||||
│ │ ├── rateLimit.helpers.js
|
||||
│ │ └── validation.helpers.js
|
||||
│ ├── routes/ # API route definitions
|
||||
│ │ ├── download.route.js
|
||||
│ │ └── info.route.js
|
||||
│ └── services/ # Business logic layer
|
||||
│ ├── download.services.js
|
||||
│ └── info.services.js
|
||||
├── theme/ # Theme system
|
||||
│ ├── template.theme.json # Theme template
|
||||
│ ├── Dark/ # Default dark theme
|
||||
│ │ └── dark.theme.json
|
||||
│ └── Light/ # Default light theme
|
||||
│ └── light.theme.json
|
||||
├── main.js # Electron main process entry point
|
||||
├── preload.js # Electron preload script (IPC bridge)
|
||||
├── config.js # Global configuration loader
|
||||
├── package.json # Project metadata and dependencies
|
||||
├── CODE_OF_CONDUCT.md # Community guidelines
|
||||
├── CONTRIBUTING.md # Contribution guidelines
|
||||
├── LICENSE # GPLv3 license
|
||||
└── README.md # This file
|
||||
```
|
||||
> Some configuration changes may require an application restart to take effect.
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
@@ -391,7 +304,7 @@ Freedom Loader includes a web-based theme creator available at [Freedom Loader W
|
||||
### Installing a custom theme
|
||||
1. Download a `.zip` theme file
|
||||
2. Drop it in the `theme/` folder of your Freedom Loader installation
|
||||
3. Restart the app — your theme appears in the settings panel
|
||||
3. Restart the app - your theme appears in the settings panel
|
||||
|
||||
## Technology Stack
|
||||
|
||||
@@ -414,28 +327,7 @@ Freedom Loader includes a web-based theme creator available at [Freedom Loader W
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 16.x or higher
|
||||
- npm or yarn
|
||||
- Git
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/MasterAcnolo/Freedom-Loader.git
|
||||
cd Freedom-Loader
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run in development mode
|
||||
npm start
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
```
|
||||
If you are interested by running a local non compiled version of Freedom Loader or just interested by contributing, see [DEVELOPMENT](./DEVELOPMENT.md)
|
||||
|
||||
### Additional Dependencies
|
||||
|
||||
@@ -446,40 +338,19 @@ You must download the required binaries and place them in the `resources` folder
|
||||
|
||||
#### Required binaries
|
||||
|
||||
- **Deno**
|
||||
- Download from: https://sourceforge.net/projects/deno.mirror/files/latest/download
|
||||
- Rename to: `deno.exe`
|
||||
|
||||
- **FFmpeg**
|
||||
- Download from: https://www.ffmpeg.org/download.html
|
||||
- Required files:
|
||||
- `ffmpeg.exe`
|
||||
- `ffprobe.exe`
|
||||
|
||||
- **yt-dlp**
|
||||
- Already bundled with the project
|
||||
- No manual installation required
|
||||
|
||||
Final folder structure:
|
||||
|
||||
```
|
||||
resources/
|
||||
├── deno.exe
|
||||
├── ffmpeg.exe
|
||||
├── ffprobe.exe
|
||||
└── yt-dlp.exe
|
||||
```
|
||||
|
||||
> These binaries are required for the application to start correctly.
|
||||
> If any of them are missing, an error message will be displayed at application startup.
|
||||
See [BINARIES.md](./BINARIES.md) for how to setup the binaries.
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
- Follow existing code style and conventions
|
||||
- Write clear commit messages
|
||||
- Test thoroughly before submitting changes
|
||||
- Update documentation when adding features
|
||||
- Maintain compatibility with Windows 10+ and Linux
|
||||
- Write clear, imperative commit messages (e.g., "Add playlist indexing" not "Added")
|
||||
- Test thoroughly on both Windows and Linux before submitting changes
|
||||
- Update documentation (README, DEVELOPMENT.md) when adding features
|
||||
- Maintain compatibility with:
|
||||
- Windows 10+
|
||||
- Fedora 43+ / Debian 11+ / Ubuntu 20.04+
|
||||
- GNOME and KDE desktop environments (when possible)
|
||||
- Consider Linux distribution differences early (see [DEVELOPMENT.md](./DEVELOPMENT.md#linux-development-considerations))
|
||||
|
||||
## Roadmap
|
||||
|
||||
@@ -521,15 +392,7 @@ Open a feature request issue with:
|
||||
5. Update documentation as needed
|
||||
6. Submit a pull request with a detailed description
|
||||
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
|
||||
|
||||
## Support
|
||||
|
||||
If you find this project useful and want to support its development:
|
||||
|
||||
[](https://paypal.me/axelnicolas25)
|
||||
|
||||
Your support helps maintain infrastructure, develop new features, and keep the project free and ad-free.
|
||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md), [DEVELOPMENT.md](./DEVELOPMENT.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing.
|
||||
|
||||
## License
|
||||
|
||||
@@ -541,12 +404,12 @@ You are free to use, modify, and redistribute this software under the terms of t
|
||||
|
||||
- [yt-dlp](https://github.com/yt-dlp/yt-dlp) for the robust download engine
|
||||
- [FFmpeg](https://ffmpeg.org/) for media processing capabilities
|
||||
- [Deno](https://deno.com/) for helping me bypassing challenges
|
||||
- [Deno](https://deno.com/) for helping me to bypass challenges
|
||||
- [Electron](https://www.electronjs.org/) for the desktop application framework
|
||||
- The open-source community for continuous support and contributions
|
||||
- [@SpicyFire21](https://github.com/SpicyFire21) to be the spiciest one
|
||||
- [@AureCX](https://github.com/AureCX) for being the Official Windows Tester!
|
||||
- Zakaria for the website icon
|
||||
- Jacques Chirac to love Apples
|
||||
- All users who test, report issues, and help improve the application
|
||||
|
||||
---
|
||||
|
||||
69
SECURITY.md
69
SECURITY.md
@@ -2,15 +2,14 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
We actively maintain and provide security updates for the following versions of Freedom Loader:
|
||||
Freedom Loader follows a **latest-only** support model. Security fixes are applied to the current release only — no backports to older versions.
|
||||
|
||||
| Version | Supported |
|
||||
|---------|--------------------|
|
||||
| 1.4.x | :white_check_mark: |
|
||||
| 1.3.x | :white_check_mark: |
|
||||
| < 1.3.0 | :x: |
|
||||
| ------------- | ------------------ |
|
||||
| Latest stable | :white_check_mark: |
|
||||
| Older versions | :x: |
|
||||
|
||||
**Note**: We recommend always using the latest version to benefit from the most recent security patches and features.
|
||||
I recommend always running the latest version. The built-in auto-updater will notify you when a new release is available. Older versions will not receive security patches regardless of severity.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
@@ -32,37 +31,40 @@ Instead, please report security issues privately by:
|
||||
|
||||
### What to Expect
|
||||
|
||||
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours
|
||||
- **Assessment**: We will assess the vulnerability and determine its severity
|
||||
- **Updates**: We will keep you informed of our progress
|
||||
- **Resolution**: We aim to release a fix within 7-14 days for critical vulnerabilities
|
||||
- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous)
|
||||
- **Acknowledgment**: I will acknowledge receipt of your report within 48 hours
|
||||
- **Assessment**: I will assess the vulnerability and determine its severity
|
||||
- **Updates**: I will keep you informed of our progress
|
||||
- **Resolution**: I aim to release a fix within 7-14 days for critical vulnerabilities
|
||||
- **Credit**: I will credit you in the release notes (unless you prefer to remain anonymous)
|
||||
|
||||
## Security Best Practices for Users
|
||||
|
||||
When using Freedom Loader, we recommend:
|
||||
When using Freedom Loader, I recommend:
|
||||
|
||||
### General Security
|
||||
|
||||
- **Keep Updated**: Always use the latest version of Freedom Loader
|
||||
- **Official Sources**: Download only from official releases on [GitHub](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
- **Verify Downloads**: Check that installers are properly signed (Windows SmartScreen may show warnings for new releases)
|
||||
- **Keep Updated**: Always use the latest version of Freedom Loader — older versions receive no security patches
|
||||
- **Official Sources**: Download only from official sources:
|
||||
- [GitHub Releases](https://github.com/MasterAcnolo/Freedom-Loader/releases)
|
||||
- [Fedora COPR](https://copr.fedorainfracloud.org/coprs/masteracnolo/freedom-loader/)
|
||||
- [Snap Store](https://snapcraft.io/freedom-loader)
|
||||
- **Verify Downloads**: Check that installers are from one of the above sources
|
||||
- **Antivirus**: Keep your antivirus software up to date
|
||||
|
||||
### Configuration Security
|
||||
|
||||
- **Download Path**: Only set download paths within your user directory (`C:\Users\[USERNAME]\...`)
|
||||
- **Download Path**: Only set download paths within your user directory
|
||||
- **Cookies**: Be aware that Firefox cookies are used for authentication — keep Firefox secure
|
||||
- **Logs**: Logs may contain sensitive information—avoid sharing them publicly without review
|
||||
- **Logs**: Logs may contain sensitive information such as URLs or local paths — avoid sharing them publicly without review
|
||||
|
||||
### Privacy Considerations
|
||||
|
||||
Freedom Loader respects your privacy:
|
||||
|
||||
- **No Data Collection**: We don't collect, store, or transmit your personal data
|
||||
- **No Data Collection**: I don't collect, store, or transmit your personal data
|
||||
- **No Telemetry**: No usage tracking or analytics
|
||||
- **Local Operation**: All downloads are processed locally on your machine
|
||||
- **Optional Features**: Discord RPC is optional and can be disabled
|
||||
- **Optional Features**: Discord RPC is optional and can be disabled in settings
|
||||
|
||||
## Known Security Considerations
|
||||
|
||||
@@ -71,16 +73,16 @@ Freedom Loader respects your privacy:
|
||||
Freedom Loader accesses Firefox cookies to download protected content. This is:
|
||||
|
||||
- **By Design**: Required for age-restricted or member-only content
|
||||
- **Local Only**: Cookies are read locally and never transmitted
|
||||
- **User Controlled**: You can control what content you download
|
||||
- **Local Only**: Cookies are read locally and never transmitted outside your machine
|
||||
- **User Controlled**: You control what content you download
|
||||
|
||||
### Native Dependencies
|
||||
|
||||
Freedom Loader bundles native binaries:
|
||||
Freedom Loader bundles the following official binaries:
|
||||
|
||||
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp)
|
||||
- **yt-dlp**: Official builds from [yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp) — auto-updated on each launch
|
||||
- **FFmpeg**: Official builds from [FFmpeg.org](https://ffmpeg.org/)
|
||||
- **Deno**: Official builds from [Deno.land](https://deno.land/)
|
||||
- **Deno**: Official builds from [Deno.com](https://deno.com/)
|
||||
|
||||
These dependencies are verified and updated regularly.
|
||||
|
||||
@@ -91,13 +93,13 @@ New releases may trigger Windows Defender warnings because:
|
||||
- The application is not yet widely distributed
|
||||
- Code signing certificates are expensive for open-source projects
|
||||
|
||||
This is expected behavior for new releases. The warning will decrease as more users download the software.
|
||||
This is expected behavior for new releases and will diminish as more users download the software.
|
||||
|
||||
## Vulnerability Disclosure Policy
|
||||
|
||||
### Our Commitment
|
||||
|
||||
We are committed to:
|
||||
I am committed to:
|
||||
|
||||
- Responding promptly to security reports
|
||||
- Working with security researchers to verify and address issues
|
||||
@@ -107,9 +109,9 @@ We are committed to:
|
||||
### Disclosure Timeline
|
||||
|
||||
1. **Day 0**: Vulnerability reported privately
|
||||
2. **Day 1-2**: Acknowledgment sent to reporter
|
||||
3. **Day 3-7**: Vulnerability assessed and fix developed
|
||||
4. **Day 7-14**: Fix released (critical vulnerabilities prioritized)
|
||||
2. **Day 1–2**: Acknowledgment sent to reporter
|
||||
3. **Day 3–7**: Vulnerability assessed and fix developed
|
||||
4. **Day 7–14**: Fix released (critical vulnerabilities prioritized)
|
||||
5. **Day 14+**: Public disclosure (coordinated with reporter)
|
||||
|
||||
### Scope
|
||||
@@ -129,16 +131,16 @@ The following are generally not considered security vulnerabilities:
|
||||
- Issues requiring physical access to the user's machine
|
||||
- Social engineering attacks
|
||||
- Denial of service against third-party services
|
||||
- Issues in third-party dependencies (report to upstream projects)
|
||||
- Missing security headers on local HTTP server (no remote access)
|
||||
- Issues in third-party dependencies (report to upstream projects directly)
|
||||
- Missing security headers on the local HTTP server (not exposed remotely)
|
||||
|
||||
## Security Updates
|
||||
|
||||
Security updates are released as:
|
||||
|
||||
- **Patch Releases**: For critical security fixes (e.g., 1.4.1 → 1.4.2)
|
||||
- **Patch Releases**: For critical security fixes (e.g., 1.6.0 → 1.6.1)
|
||||
- **Automatic Updates**: Users are notified via the built-in update system
|
||||
- **Release Notes**: Security fixes are clearly marked in changelog
|
||||
- **Release Notes**: Security fixes are clearly marked in the changelog
|
||||
|
||||
## Additional Resources
|
||||
|
||||
@@ -150,6 +152,7 @@ Security updates are released as:
|
||||
|
||||
For security-related questions or concerns:
|
||||
|
||||
- **Email**: masteracnolo25@gmail.com
|
||||
- **GitHub**: [@MasterAcnolo](https://github.com/MasterAcnolo)
|
||||
|
||||
---
|
||||
|
||||
@@ -121,8 +121,14 @@ function initAutoUpdater(mainWindow) {
|
||||
*/
|
||||
async function checkForUpdates() {
|
||||
|
||||
if (process.env.SNAP) {
|
||||
logger.info("Running as Snap, update managed by snapd, skipping electron-updater");
|
||||
/**
|
||||
* Prevents electron-updater from running inside sandboxed environments.
|
||||
* Snap and Flatpak lock the file system in read-only mode, causing the updater to crash.
|
||||
* In these environments, updates are safely handled by snapd or the Flatpak runtime.
|
||||
* Standalone Linux packages (.deb, .rpm, AppImage) will bypass this and update normally.
|
||||
*/
|
||||
if (process.env.SNAP || process.env.FLATPAK_ID) {
|
||||
logger.info("Running as Snap or Flatpak. Updates are managed by the OS store. Skipping electron-updater.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
const config = require('../config');
|
||||
const RPC = require("discord-rpc");
|
||||
const { logger } = require("../server/logger");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
/**
|
||||
* Discord Application Client ID used to authenticate the RPC connection.
|
||||
@@ -20,6 +22,51 @@ const rpc = new RPC.Client({ transport: "ipc" });
|
||||
*/
|
||||
let intervalId;
|
||||
|
||||
/**
|
||||
* Automagically fixes Discord RPC on Linux for Sandboxed versions (Flatpak & Snap).
|
||||
* It creates a symlink from the sandbox IPC socket to the standard system location.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function fixLinuxDiscordRPC() {
|
||||
// Only execute on Linux
|
||||
if (process.platform !== "linux") return;
|
||||
|
||||
/**
|
||||
* Get the user's runtime directory (e.g., /run/user/1000).
|
||||
* @type {string}
|
||||
*/
|
||||
const xdgDir = process.env.XDG_RUNTIME_DIR || `/run/user/${process.getuid()}`;
|
||||
const standardSocket = path.join(xdgDir, "discord-ipc-0");
|
||||
|
||||
// If the standard socket already exists (Native Discord), do nothing.
|
||||
if (fs.existsSync(standardSocket)) return;
|
||||
|
||||
/**
|
||||
* Paths for known sandboxed Discord IPC sockets.
|
||||
*/
|
||||
const flatpakSocket = path.join(xdgDir, "app/com.discordapp.Discord/discord-ipc-0");
|
||||
const snapSocket = path.join(xdgDir, "snap.discord/discord-ipc-0");
|
||||
|
||||
let targetSocket = null;
|
||||
|
||||
if (fs.existsSync(flatpakSocket)) {
|
||||
targetSocket = flatpakSocket;
|
||||
} else if (fs.existsSync(snapSocket)) {
|
||||
targetSocket = snapSocket;
|
||||
}
|
||||
|
||||
// If a sandboxed socket is found, create the symlink stealthily.
|
||||
if (targetSocket) {
|
||||
try {
|
||||
fs.symlinkSync(targetSocket, standardSocket);
|
||||
logger.info(`Discord RPC bridge created automatically targeting: ${targetSocket}`);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to create Discord RPC bridge: ${err.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes Discord Rich Presence (RPC) connection.
|
||||
*
|
||||
@@ -32,6 +79,11 @@ let intervalId;
|
||||
*/
|
||||
function startRPC() {
|
||||
|
||||
/**
|
||||
* Patch that create Simlinks for Linux environment;
|
||||
*/
|
||||
fixLinuxDiscordRPC();
|
||||
|
||||
/**
|
||||
* Rich Presence payload describing current application state
|
||||
* shown in Discord user profile.
|
||||
@@ -50,8 +102,10 @@ function startRPC() {
|
||||
|
||||
});
|
||||
|
||||
rpc.login({ clientId }).catch(err => {
|
||||
logger.error("Unable to connect to the RPC:", err);
|
||||
rpc.login({ clientId }).catch(() => {
|
||||
// Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error.
|
||||
// logger.error("Unable to connect to the RPC:", err);
|
||||
logger.info("Unable to connect to the Discord RPC. Discord is maybe not launched");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,26 +114,21 @@ function startRPC() {
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Clear any running intervals
|
||||
* - Remove current activity from Discord
|
||||
* - Remove current activity from Discord if connected
|
||||
* - Destroy RPC transport connection
|
||||
* - Handle cleanup errors safely
|
||||
*/
|
||||
async function stopRPC() {
|
||||
if (rpc) {
|
||||
try {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
|
||||
/**
|
||||
* Ensures RPC connection exists before attempting cleanup.
|
||||
*/
|
||||
if (rpc && rpc.transport) {
|
||||
await rpc.clearActivity()
|
||||
await rpc.destroy();
|
||||
} else{
|
||||
logger.error("Not Able to close RPC")
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
logger.error("Error while closing the RPC:", err);
|
||||
// Since 1.6.2, it no longer print the error. This is because if discord is not opened, it will send an error.
|
||||
// logger.error("Error while closing the RPC:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
const { ipcMain, dialog, shell } = require("electron");
|
||||
const fs = require("fs");
|
||||
const { logger, logDir } = require("../server/logger");
|
||||
const { configFeatures, featuresPath } = require("../config");
|
||||
const {configFeatures, featuresPath, devMode} = require("../config");
|
||||
const { getThemes, reloadThemes } = require("./themeManager");
|
||||
const config = require("../config");
|
||||
const { validateDownloadPath, getDefaultDownloadPath } = require("./pathValidator");
|
||||
const { userThemesPath } = require("../server/helpers/path.helpers");
|
||||
const { createSystemTray, destroyTray } = require("./tray");
|
||||
|
||||
/**
|
||||
* Security whitelist for feature flags that can be modified at runtime.
|
||||
@@ -27,6 +28,7 @@ const { userThemesPath } = require("../server/helpers/path.helpers");
|
||||
*/
|
||||
const FEATURE_WHITELIST = new Set([
|
||||
"autoUpdate",
|
||||
"systemTray",
|
||||
"discordRPC",
|
||||
"customTopBar",
|
||||
"autoCheckInfo",
|
||||
@@ -34,6 +36,7 @@ const FEATURE_WHITELIST = new Set([
|
||||
"addMetadata",
|
||||
"verboseLogs",
|
||||
"autoDownloadPlaylist",
|
||||
"keepPlaylistOrder",
|
||||
"customCodec",
|
||||
"theme",
|
||||
"createPlaylistFolders",
|
||||
@@ -111,7 +114,18 @@ function registerIpcHandlers(getMainWindow) {
|
||||
/**
|
||||
* Window minimize request from renderer.
|
||||
*/
|
||||
ipcMain.on("window-minimize", () => getMainWindow()?.minimize());
|
||||
ipcMain.on("window-minimize", () => {
|
||||
// Minimize to tray
|
||||
if (configFeatures.systemTray) {
|
||||
getMainWindow()?.hide()
|
||||
logger.info("Window Minimized in SystemTray (Using Minimize Button)");
|
||||
} else {
|
||||
// Native minimize
|
||||
getMainWindow()?.minimize()
|
||||
logger.info("Window minimized normally");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Toggles maximize/unmaximize state of main window.
|
||||
@@ -212,6 +226,21 @@ function registerIpcHandlers(getMainWindow) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dynamically intercepts and applies System Tray state changes at runtime.
|
||||
* Instantiates or destroys the tray icon immediately to prevent window
|
||||
* lifecycle desynchronization (e.g., minimizing a window to a non-existent tray).
|
||||
*/
|
||||
if (key === "systemTray") {
|
||||
if (value === true) {
|
||||
logger.info("System Tray enabled dynamically.");
|
||||
createSystemTray(devMode);
|
||||
} else {
|
||||
logger.info("System Tray disabled dynamically.");
|
||||
destroyTray();
|
||||
}
|
||||
}
|
||||
|
||||
configFeatures[key] = value;
|
||||
|
||||
fs.writeFileSync(
|
||||
|
||||
@@ -56,28 +56,23 @@ function validateDownloadPath(userPath) {
|
||||
if (!userPath) return getDefaultDownloadPath();
|
||||
|
||||
try {
|
||||
const absolutePath = path.resolve(userPath);
|
||||
|
||||
/**
|
||||
* Resolves filesystem symlinks to ensure canonical absolute path.
|
||||
* Required to prevent path traversal or alias bypass.
|
||||
*/
|
||||
const resolved = fs.realpathSync(path.resolve(userPath));
|
||||
|
||||
if (!fs.existsSync(resolved)) {
|
||||
fs.mkdirSync(resolved, { recursive: true });
|
||||
logger.info(`Download folder created: ${resolved}`);
|
||||
if (!fs.existsSync(absolutePath)) {
|
||||
fs.mkdirSync(absolutePath, { recursive: true });
|
||||
logger.info(`Download folder created: ${absolutePath}`);
|
||||
}
|
||||
|
||||
const real = fs.realpathSync(resolved);
|
||||
const real = fs.realpathSync(absolutePath);
|
||||
|
||||
if (!isSafePath(resolved)) {
|
||||
throw new Error("Path not allowed: system folders are blocked!");
|
||||
if (!isSafePath(real)) {
|
||||
logger.error("Path not allowed: system folders are blocked!");
|
||||
}
|
||||
|
||||
return real;
|
||||
}
|
||||
catch (err) {
|
||||
|
||||
catch (err) {
|
||||
/**
|
||||
* Handles invalid filesystem paths, permissions issues, or resolution failures.
|
||||
* Logs diagnostic information before propagating a sanitized error.
|
||||
|
||||
117
app/tray.js
Normal file
117
app/tray.js
Normal file
@@ -0,0 +1,117 @@
|
||||
const {app, Menu, Tray, nativeImage} = require("electron");
|
||||
const path = require("path");
|
||||
const {logger} = require("../server/logger"); // Ajuste le chemin si besoin
|
||||
const fs = require("fs");
|
||||
const {getMainWindow} = require("./windowManager");
|
||||
|
||||
/**
|
||||
* Global reference to the Tray instance to prevent garbage collection.
|
||||
* @type {Tray | null}
|
||||
*/
|
||||
let tray = null;
|
||||
|
||||
/**
|
||||
* Creates and configures the cross-platform System Tray.
|
||||
*
|
||||
* Responsibilities:
|
||||
* - Loads the appropriate icon format based on the operating system.
|
||||
* - Builds the right-click context menu.
|
||||
* - Handles left-click behavior to toggle main window visibility.
|
||||
*
|
||||
* @param {boolean} devMode - Indicates whether the application is running in development mode.
|
||||
* @returns {Tray} The created Tray instance.
|
||||
*/
|
||||
function createSystemTray(devMode) {
|
||||
// Prevent creating multiple tray instances
|
||||
if (tray) return tray;
|
||||
|
||||
const isWin = process.platform === "win32";
|
||||
const iconName = isWin ? "app-icon.ico" : "app-icon-64x64.png";
|
||||
|
||||
// Attempt to load the icon from the local build directory first
|
||||
let iconPath = path.join(__dirname, "..", "build", iconName);
|
||||
|
||||
// Fallback to the extracted resources path if running in production
|
||||
if (!devMode && !fs.existsSync(iconPath)) {
|
||||
iconPath = path.join(process.resourcesPath, iconName);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(iconPath)) {
|
||||
logger.error(`System Tray icon not found at resolved path: ${iconPath}`);
|
||||
} else {
|
||||
logger.info("System Tray icon resolved successfully.");
|
||||
}
|
||||
|
||||
const icon = nativeImage.createFromPath(iconPath);
|
||||
|
||||
if (icon.isEmpty()) {
|
||||
logger.error("System Tray icon file was found but could not be decoded by Electron (image is empty).");
|
||||
}
|
||||
|
||||
tray = new Tray(icon);
|
||||
tray.setToolTip("Freedom Loader");
|
||||
|
||||
/**
|
||||
* System Tray Context Menu (Right-click action).
|
||||
*/
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Show Freedom Loader",
|
||||
click: () => {
|
||||
const window = getMainWindow();
|
||||
if (window && !window.isDestroyed()) {
|
||||
window.show();
|
||||
window.focus();
|
||||
logger.info("Window Restored from SystemTray (Using Show Action)");
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: "Quit",
|
||||
click: () => {
|
||||
/**
|
||||
* Flags the application for a deliberate exit,
|
||||
* bypassing the default "minimize to tray" behavior.
|
||||
*/
|
||||
app.isQuitting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
tray.setContextMenu(contextMenu);
|
||||
|
||||
/**
|
||||
* Left-click behavior: Toggles main window visibility.
|
||||
* but it remains standard practice for Windows and Linux environments.
|
||||
*/
|
||||
tray.on("click", () => {
|
||||
const window = getMainWindow();
|
||||
if (!window || window.isDestroyed()) return;
|
||||
|
||||
if (window.isVisible()) {
|
||||
window.hide();
|
||||
} else {
|
||||
window.show();
|
||||
window.focus();
|
||||
logger.info("Window Restored from SystemTray (Double Click on Tray Icon)")
|
||||
}
|
||||
});
|
||||
|
||||
return tray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely destroys the tray icon to prevent OS-level crashes (especially on Linux/Wayland).
|
||||
* Should be called right before the application quits.
|
||||
*/
|
||||
function destroyTray() {
|
||||
if (tray && !tray.isDestroyed()) {
|
||||
tray.destroy();
|
||||
tray = null;
|
||||
logger.info("System Tray destroyed cleanly.");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {createSystemTray, destroyTray};
|
||||
@@ -81,6 +81,21 @@ async function createMainWindow() {
|
||||
throw err;
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts the window close event.
|
||||
* If the app is not explicitly quitting (via Tray menu),
|
||||
* cancels the destruction and hides the window instead.
|
||||
*/
|
||||
mainWindow.on('close', (event) => {
|
||||
|
||||
if (!app.isQuitting && config.configFeatures.systemTray) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
logger.info("Window Minimized in SystemTray (Using Close Button)");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Cleans up window reference when the main window is closed.
|
||||
* Prevents memory leaks and allows recreation if needed.
|
||||
|
||||
BIN
build/app-icon-64x64.png
Normal file
BIN
build/app-icon-64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
26
config.js
26
config.js
@@ -7,7 +7,7 @@ const path = require("path");
|
||||
* Indicates whether the application is running in development mode.
|
||||
* Determined by Electron's packaging state.
|
||||
*/
|
||||
const devMode = !app.isPackaged;
|
||||
const devMode = !app.isPackaged || process.env.NODE_ENV === "test";
|
||||
|
||||
/**
|
||||
* Resolves the configuration file path depending on runtime environment.
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"autoUpdate": true,
|
||||
"systemTray": false,
|
||||
"discordRPC": true,
|
||||
"customTopBar": true,
|
||||
"autoCheckInfo": true,
|
||||
@@ -7,7 +8,8 @@
|
||||
"addMetadata": true,
|
||||
"verboseLogs": false,
|
||||
"autoDownloadPlaylist": true,
|
||||
"createPlaylistFolders": true,
|
||||
"keepPlaylistOrder": false,
|
||||
"createPlaylistFolders": false,
|
||||
"customCodec": "h264",
|
||||
"logSystem": true,
|
||||
"outputTitleCheck": true,
|
||||
|
||||
21
jest.config.js
Normal file
21
jest.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
testEnvironment: "node",
|
||||
testMatch: ["**/test/unit/**/*.test.js"],
|
||||
collectCoverageFrom: [
|
||||
"server/**/*.js",
|
||||
"app/**/*.js",
|
||||
"!app/autoUpdater.js",
|
||||
],
|
||||
|
||||
modulePathIgnorePatterns: [
|
||||
"<rootDir>/dist",
|
||||
"<rootDir>/release",
|
||||
"<rootDir>/build",
|
||||
],
|
||||
|
||||
watchPathIgnorePatterns: [
|
||||
"<rootDir>/dist",
|
||||
"<rootDir>/release",
|
||||
"<rootDir>/build",
|
||||
],
|
||||
};
|
||||
85
main.js
85
main.js
@@ -37,11 +37,14 @@ app.setAppUserModelId("com.masteracnolo.freedomloader");
|
||||
/**
|
||||
* Load the app dependencies for hardware choice
|
||||
*/
|
||||
const { logger, logSessionStart, logSessionEnd } = require("./server/logger");
|
||||
const { configFeatures } = require("./config");
|
||||
const { logger, logSessionStart, logSessionEnd, logDir } = require("./server/logger");
|
||||
const { configFeatures, devMode} = require("./config");
|
||||
|
||||
/**
|
||||
* On start, enable or disable hardWareAcceleration
|
||||
* In-memory snapshot of application feature flags.
|
||||
*
|
||||
* Note: Changes to the config file are not automatically reflected.
|
||||
* A restart could be required.
|
||||
*/
|
||||
if (!configFeatures.enableHardwareAcceleration){
|
||||
app.disableHardwareAcceleration();
|
||||
@@ -50,15 +53,7 @@ if (!configFeatures.enableHardwareAcceleration){
|
||||
logger.info("Enable Hardware Acceleration")
|
||||
}
|
||||
|
||||
/**
|
||||
* In-memory snapshot of application feature flags.
|
||||
*
|
||||
* Note: Changes to the config file are not automatically reflected.
|
||||
* A restart could be required.
|
||||
*/
|
||||
const config = require("./config");
|
||||
|
||||
if(config.devMode){
|
||||
if(devMode){
|
||||
/**
|
||||
* Start devTron extensions - @see https://github.com/electron/devtron
|
||||
*/
|
||||
@@ -79,8 +74,45 @@ const { updateYtDlp } = require("./app/ytDlpUpdater");
|
||||
const { createMainWindow, getMainWindow } = require("./app/windowManager");
|
||||
const { registerIpcHandlers } = require("./app/ipcHandlers");
|
||||
const { createSplashWindow, closeSplashWindow, setSplashProgress } = require("./app/splashManager");
|
||||
const { userThemesPath, initUserThemes, isWindows } = require("./server/helpers/path.helpers");
|
||||
const { userThemesPath, initUserThemes, validateBinaries, defaultDownloadFolder } = require("./server/helpers/path.helpers");
|
||||
const {createSystemTray, destroyTray} = require("./app/tray");
|
||||
const { stopServer } = require("./server/server");
|
||||
|
||||
/**
|
||||
* Global flag indicating if the application is intentionally shutting down.
|
||||
* Used across the app to bypass the "minimize to tray on close" behavior.
|
||||
* @type {boolean}
|
||||
*/
|
||||
app.isQuitting = false;
|
||||
|
||||
/**
|
||||
* State flag to track the asynchronous cleanup process during shutdown.
|
||||
* Prevents the application from exiting before services (RPC, Tray, Logs) are properly closed.
|
||||
* @type {boolean}
|
||||
*/
|
||||
let isCleanedUp = false;
|
||||
|
||||
/**
|
||||
* Performs a graceful shutdown of all application services asynchronously.
|
||||
* Once finished, it re-triggers the app.quit() process.
|
||||
*/
|
||||
async function cleanShutdown() {
|
||||
try {
|
||||
destroyTray();
|
||||
await stopRPC();
|
||||
stopServer();
|
||||
|
||||
} catch (err) {
|
||||
logger.error("Error during cleanup:", err);
|
||||
|
||||
} finally {
|
||||
logger.info("All services stopped. Have a nice day!");
|
||||
logSessionEnd();
|
||||
|
||||
isCleanedUp = true;
|
||||
app.quit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If another instance want to run
|
||||
@@ -99,11 +131,11 @@ app.on("second-instance", () => {
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
logSessionStart();
|
||||
logSessionStart(logDir, defaultDownloadFolder);
|
||||
|
||||
createSplashWindow();
|
||||
|
||||
if (!config.devMode && !checkNativeDependencies()) return;
|
||||
if (!devMode && !checkNativeDependencies()) return;
|
||||
|
||||
const { userYtDlp } = require("./server/helpers/path.helpers");
|
||||
updateYtDlp(userYtDlp);
|
||||
@@ -117,6 +149,8 @@ app.whenReady().then(async () => {
|
||||
|
||||
setSplashProgress(2); // Loading themes
|
||||
|
||||
validateBinaries();
|
||||
|
||||
initUserThemes();
|
||||
await initThemes(userThemesPath);
|
||||
|
||||
@@ -125,6 +159,11 @@ app.whenReady().then(async () => {
|
||||
|
||||
closeSplashWindow();
|
||||
getMainWindow().show();
|
||||
|
||||
if (configFeatures.systemTray) {
|
||||
createSystemTray(devMode);
|
||||
}
|
||||
|
||||
if (configFeatures.discordRPC) startRPC();
|
||||
|
||||
if (configFeatures.autoUpdate) initAutoUpdater(getMainWindow());
|
||||
@@ -136,12 +175,20 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (app.isQuitting || !configFeatures.systemTray) {
|
||||
logger.info("Shutting down...");
|
||||
app.quit();
|
||||
} else if (process.platform !== "darwin") {
|
||||
logger.info("Main window closed, app running in background (Tray).");
|
||||
}
|
||||
});
|
||||
|
||||
app.on("before-quit", async () => {
|
||||
await stopRPC();
|
||||
logger.info("All services stopped. Have a nice day!");
|
||||
logSessionEnd();
|
||||
app.on("before-quit", (event) => {
|
||||
app.isQuitting = true;
|
||||
|
||||
if (!isCleanedUp) {
|
||||
event.preventDefault();
|
||||
|
||||
cleanShutdown();
|
||||
}
|
||||
});
|
||||
6141
package-lock.json
generated
6141
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
113
package.json
113
package.json
@@ -1,17 +1,37 @@
|
||||
{
|
||||
"name": "freedom-loader",
|
||||
"desktopName": "com.masteracnolo.freedomloader",
|
||||
"productName": "Freedom Loader",
|
||||
"version": "1.6.0",
|
||||
"version": "1.6.3",
|
||||
"author": "MasterAcnolo <MasterAcnolo@users.noreply.github.com>",
|
||||
"description": "Freedom Loader",
|
||||
"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/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/MasterAcnolo/Freedom-Loader.git"
|
||||
},
|
||||
"license": "GPL-3.0-only",
|
||||
"bugs": {
|
||||
"url": "https://github.com/MasterAcnolo/Freedom-Loader/issues"
|
||||
},
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"start": "electron --trace-warnings .",
|
||||
"postinstall": "chmod +x resources/binaries/linux/* 2>/dev/null || true",
|
||||
"start": "electron --no-warnings .",
|
||||
"start:warn": "electron --trace-warnings .",
|
||||
"start:debug": "WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .",
|
||||
"dev": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --no-warnings .\"",
|
||||
"dev:warn": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"electron --trace-warnings .\"",
|
||||
"dev:debug": "nodemon --watch server --watch app --watch main.js --watch config.js --ext js,json --exec \"WAYLAND_DEBUG=1 ELECTRON_ENABLE_LOGGING=1 ELECTRON_ENABLE_STACK_DUMPING=1 electron --enable-logging --v=1 .\"",
|
||||
"build": "electron-builder",
|
||||
"build:win": "electron-builder --win",
|
||||
"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 test/unit",
|
||||
"update": "npm update"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -27,9 +47,14 @@
|
||||
"winston-daily-rotate-file": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^41.2.0",
|
||||
"electron-builder": "^26.8.1",
|
||||
"@electron/devtron": "^2.1.1"
|
||||
"@electron/devtron": "^2.1.1",
|
||||
"@jest/globals": "^30.4.1",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"electron": "^44.3.0",
|
||||
"electron-builder": "^26.15.3",
|
||||
"jest": "^30.4.2",
|
||||
"nodemon": "^3.1.14",
|
||||
"playwright": "^1.62.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.masteracnolo.freedomloader",
|
||||
@@ -53,22 +78,42 @@
|
||||
},
|
||||
"asar": true,
|
||||
"extraResources": [
|
||||
{ "from": "theme", "to": "theme" },
|
||||
{ "from": "build/confirm-icon.png", "to": "confirm-icon.png" },
|
||||
{ "from": "build/banner.bmp", "to": "banner.bmp" },
|
||||
{ "from": "build/banner.png", "to": "banner.png" },
|
||||
{ "from": "build/error.png", "to": "error.png" },
|
||||
{ "from": "config/config.default.json", "to": "config/config.default.json" }
|
||||
{
|
||||
"from": "theme",
|
||||
"to": "theme"
|
||||
},
|
||||
{
|
||||
"from": "config/config.default.json",
|
||||
"to": "config/config.default.json"
|
||||
},
|
||||
{
|
||||
"from": "build",
|
||||
"to": ".",
|
||||
"filter": [
|
||||
"app-icon.ico",
|
||||
"app-icon-64x64.png",
|
||||
"confirm-icon.png",
|
||||
"banner.bmp",
|
||||
"banner.png",
|
||||
"error.png"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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", "to": "binaries/yt-dlp.exe" },
|
||||
{ "from": "resources/binaries/win-32/ffmpeg.exe", "to": "binaries/ffmpeg.exe" },
|
||||
{ "from": "resources/binaries/win-32/ffprobe.exe", "to": "binaries/ffprobe.exe" },
|
||||
{ "from": "resources/binaries/win-32/deno.exe", "to": "binaries/deno.exe" }
|
||||
{
|
||||
"from": "resources/binaries/win-32",
|
||||
"to": "binaries",
|
||||
"filter": [
|
||||
"yt-dlp.exe",
|
||||
"ffmpeg.exe",
|
||||
"ffprobe.exe",
|
||||
"deno.exe"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"nsis": {
|
||||
@@ -83,14 +128,27 @@
|
||||
"shortcutName": "Freedom Loader"
|
||||
},
|
||||
"linux": {
|
||||
"target": ["AppImage", "deb","rpm", "snap"],
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb",
|
||||
"snap"
|
||||
],
|
||||
"category": "Utility",
|
||||
"synopsis": "GUI for yt-dlp to download video and audio with metadata",
|
||||
"maintainer": "MasterAcnolo",
|
||||
"icon": "build/app-icon.png",
|
||||
"executableName": "freedom-loader",
|
||||
"extraResources": [
|
||||
{ "from": "resources/binaries/linux/yt-dlp", "to": "binaries/yt-dlp" },
|
||||
{ "from": "resources/binaries/linux/ffmpeg", "to": "binaries/ffmpeg" },
|
||||
{ "from": "resources/binaries/linux/ffprobe", "to": "binaries/ffprobe" },
|
||||
{ "from": "resources/binaries/linux/deno", "to": "binaries/deno" }
|
||||
{
|
||||
"from": "resources/binaries/linux",
|
||||
"to": "binaries",
|
||||
"filter": [
|
||||
"yt-dlp",
|
||||
"ffmpeg",
|
||||
"ffprobe",
|
||||
"deno"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"deb": {
|
||||
@@ -107,14 +165,7 @@
|
||||
"confinement": "strict",
|
||||
"plugs": [
|
||||
"default",
|
||||
"home",
|
||||
"removable-media",
|
||||
"network",
|
||||
"audio-playback",
|
||||
"desktop",
|
||||
"desktop-legacy",
|
||||
"x11",
|
||||
"wayland"
|
||||
"removable-media"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
9
package/copr-build.sh
Normal file
9
package/copr-build.sh
Normal file
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh -xe
|
||||
git clone --depth 1 https://github.com/MasterAcnolo/Freedom-Loader.git src
|
||||
cd src
|
||||
npm ci
|
||||
npx electron-builder --linux dir
|
||||
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
tar czf "$resultdir/freedom-loader-$VERSION.tar.gz" -C dist linux-unpacked
|
||||
sed "s/__VERSION__/$VERSION/" package/freedom-loader.spec > "$resultdir/freedom-loader.spec"
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
@@ -56,9 +56,9 @@
|
||||
<div class="setting-item" id="theme" style="margin-left: 32px;">
|
||||
<div class="setting-info">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span class="setting-title">Thème</span>
|
||||
<span class="setting-title">Theme</span>
|
||||
|
||||
<select id="themeSelect" data-key="theme" aria-label="Choisir le thème">
|
||||
<select id="themeSelect" data-key="theme" aria-label="Choose the theme">
|
||||
|
||||
</select>
|
||||
<button id="refresh-themes-btn" title="Refresh themes" class="refresh-themes-btn">
|
||||
@@ -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>
|
||||
@@ -127,6 +127,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<input data-key="systemTray" id="systemTray" type="checkbox">
|
||||
<div class="setting-info">
|
||||
<span class="setting-title">System Tray</span>
|
||||
<small>Minimize app on close, in the System Tray. [EXPERIMENTAL]</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="setting-item">
|
||||
<input type="checkbox" data-key="outputTitleCheck" id="outputTitleCheck">
|
||||
<div class="setting-info">
|
||||
@@ -135,6 +143,14 @@
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<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">
|
||||
@@ -246,7 +262,7 @@
|
||||
</div>
|
||||
|
||||
<div class="download-path">
|
||||
Will be save on <span id="savePath"></span>
|
||||
Will be saved in <span id="savePath"></span>
|
||||
<button id="changePath">Edit</button>
|
||||
</div>
|
||||
|
||||
@@ -290,7 +306,6 @@
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="script/toast.js"></script>
|
||||
<script src="script/customConfiguration.js"></script>
|
||||
<script src="script/clipboardPaste.js"></script>
|
||||
<script src="script/custompath.js"></script>
|
||||
<script src="script/settingsPanel.js"></script>
|
||||
|
||||
@@ -1,21 +1,54 @@
|
||||
/**
|
||||
* Retrieves the application version from the Electron main process
|
||||
* and displays it in the UI version badge (bottom-right corner).
|
||||
* Retrieves the application version and build state from the backend.
|
||||
* Displays a formatted string indicating packaging state and preview status.
|
||||
* Hides the "Packaged" state for production builds to keep the UI clean.
|
||||
*
|
||||
* This is mainly used for debugging and build identification.
|
||||
* Example outputs: "Not Packaged Preview v1.6.2" or "Preview v1.6.2"
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function displayVersionLabel() {
|
||||
/**
|
||||
* Raw version string from the backend (e.g., "dev-1.6.2-preview" or "v1.6.2")
|
||||
* @type {string}
|
||||
*/
|
||||
async function versionLabel() {
|
||||
const appVersion = await window.electronAPI.getVersion();
|
||||
|
||||
/**
|
||||
* UI element displaying the current application version.
|
||||
* Updated at runtime after IPC call resolves.
|
||||
* @type {HTMLElement | null}
|
||||
*/
|
||||
const versionBadge = document.getElementById("version-badge");
|
||||
|
||||
if (!versionBadge) return;
|
||||
|
||||
versionBadge.textContent = `${appVersion}`;
|
||||
/**
|
||||
* Extracts the numeric semantic version (e.g., "1.6.2").
|
||||
* @type {RegExpMatchArray | null}
|
||||
*/
|
||||
const versionMatch = appVersion.match(/(\d+\.\d+\.\d+)/);
|
||||
const baseVersion = versionMatch ? versionMatch[0] : "Unknown";
|
||||
|
||||
/**
|
||||
* Determines the packaging state. The backend prepends "dev-" if unpackaged.
|
||||
* @type {boolean}
|
||||
*/
|
||||
const isPackaged = !appVersion.includes("dev-");
|
||||
|
||||
/**
|
||||
* Determines if the current build is a preview version.
|
||||
* @type {boolean}
|
||||
*/
|
||||
const isPreview = appVersion.includes("preview");
|
||||
|
||||
/**
|
||||
* Construct the final display string.
|
||||
* If packaged, we don't mention it. If not, we explicitly say "Not Packaged ".
|
||||
*/
|
||||
const packageStateStr = isPackaged ? "" : "Not Packaged ";
|
||||
const previewStr = isPreview ? "Preview " : "";
|
||||
|
||||
versionBadge.textContent = `${packageStateStr}${previewStr}v${baseVersion}`;
|
||||
}
|
||||
|
||||
versionLabel();
|
||||
displayVersionLabel();
|
||||
@@ -1,27 +0,0 @@
|
||||
async function initPlatformRestrictions() {
|
||||
const platform = await window.electronAPI.getProcessPlatform();
|
||||
|
||||
const themeSelect = document.getElementById("themeSelect");
|
||||
const refreshBtn = document.getElementById("refresh-themes-btn");
|
||||
|
||||
/**
|
||||
* Is the OS windows
|
||||
* @type {boolean}
|
||||
*/
|
||||
const isWindows = platform === "win32";
|
||||
|
||||
console.log(isWindows);
|
||||
|
||||
// If it's windows, the button is not disabled.
|
||||
themeSelect.disabled = !isWindows;
|
||||
refreshBtn.disabled = !isWindows;
|
||||
|
||||
themeSelect.classList.toggle("disabled", !isWindows);
|
||||
refreshBtn.classList.toggle("disabled", !isWindows);
|
||||
|
||||
if (!isWindows) {
|
||||
themeSelect.title = "Themes are only available on Windows";
|
||||
refreshBtn.title = "Themes are only available on Windows";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ async function initThemes() {
|
||||
const defaultTheme = {
|
||||
id: "dark",
|
||||
name: "Dark",
|
||||
subtitle: "Darkness is my ally",
|
||||
subtitle: "Because Why Not ?",
|
||||
style: {
|
||||
background: {
|
||||
size: "cover",
|
||||
|
||||
@@ -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"}
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
if (!res.ok) return {error: `An Error occured when fetching info`};
|
||||
|
||||
const data = await res.json();
|
||||
if (!data) return { error: "Data is Missing" };
|
||||
|
||||
return data;
|
||||
|
||||
@@ -5,47 +5,41 @@
|
||||
* This function is safe to call once DOM is ready.
|
||||
* It silently aborts if the API or elements are missing.
|
||||
*/
|
||||
function setupTopbarListeners() {
|
||||
export function initTopBar() {
|
||||
const { topbarAPI } = window;
|
||||
if (!topbarAPI) return;
|
||||
|
||||
const minBtn = document.getElementById("minimize-btn");
|
||||
const maxBtn = document.getElementById("maximize-btn");
|
||||
const closeBtn = document.getElementById("close-btn");
|
||||
const devtoolsBtn = document.getElementById("devtools-btn");
|
||||
const logsBtn = document.getElementById("logs-btn");
|
||||
const websiteBtn = document.getElementById("website-btn");
|
||||
const wikiBtn = document.getElementById("wiki-btn");
|
||||
const workshopBtn = document.getElementById("workshop-btn");
|
||||
|
||||
if (minBtn) minBtn.onclick = () => topbarAPI.minimize();
|
||||
|
||||
const maxBtn = document.getElementById("maximize-btn");
|
||||
if (maxBtn) maxBtn.onclick = () => topbarAPI.maximize();
|
||||
|
||||
const closeBtn = document.getElementById("close-btn");
|
||||
if (closeBtn) closeBtn.onclick = () => topbarAPI.close();
|
||||
|
||||
const devtoolsBtn = document.getElementById("devtools-btn");
|
||||
if (devtoolsBtn) devtoolsBtn.onclick = () => topbarAPI.openDevTools();
|
||||
|
||||
const logsBtn = document.getElementById("logs-btn");
|
||||
if (logsBtn) logsBtn.onclick = () => topbarAPI.openLogs();
|
||||
|
||||
const websiteBtn = document.getElementById("website-btn");
|
||||
if (websiteBtn) websiteBtn.onclick = () => topbarAPI.openWebsite();
|
||||
|
||||
const wikiBtn = document.getElementById("wiki-btn");
|
||||
if (wikiBtn) wikiBtn.onclick = () => topbarAPI.openWiki();
|
||||
|
||||
const workshopBtn = document.getElementById("workshop-btn");
|
||||
if (workshopBtn) workshopBtn.onclick = () => topbarAPI.openWorkshop();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers topbar event listeners after DOM is fully loaded.
|
||||
*/
|
||||
document.addEventListener("DOMContentLoaded", setupTopbarListeners);
|
||||
async function attachListeners(){
|
||||
try{
|
||||
const featuresList = await window.electronAPI.getFeatures();
|
||||
|
||||
/**
|
||||
* Applies UI layout adjustments depending on feature flags
|
||||
* provided by the Electron backend.
|
||||
*
|
||||
* Specifically handles:
|
||||
* - hiding custom topbar
|
||||
* - adjusting layout spacing
|
||||
* - repositioning theme switcher
|
||||
*/
|
||||
(async function applyFeatureLayout() {
|
||||
const features = await window.electronAPI.getFeatures();
|
||||
|
||||
if (!features.customTopBar) {
|
||||
if (!featuresList.customTopBar) {
|
||||
const topbar = document.getElementById("topbar");
|
||||
const container = document.getElementById("container");
|
||||
const themeSwitcher = document.getElementById("theme-switcher");
|
||||
@@ -54,4 +48,12 @@ document.addEventListener("DOMContentLoaded", setupTopbarListeners);
|
||||
if (container) container.style.marginTop = "0";
|
||||
if (themeSwitcher) themeSwitcher.style.top = "30px";
|
||||
}
|
||||
})();
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to load layout features for topbar:", error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
initTopBar()
|
||||
document.addEventListener("DOMContentLoaded", attachListeners)
|
||||
0
resources/binaries/linux/.gitkeep
Normal file
0
resources/binaries/linux/.gitkeep
Normal file
Binary file not shown.
0
resources/binaries/win-32/.gitkeep
Normal file
0
resources/binaries/win-32/.gitkeep
Normal file
Binary file not shown.
116
scripts/create-copr-package.sh
Executable file
116
scripts/create-copr-package.sh
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
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
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 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 \
|
||||
--rpm-use-file-permissions \
|
||||
--name freedom-loader \
|
||||
--version "$VERSION" \
|
||||
--architecture x86_64 \
|
||||
--description "Free and open-source GUI for yt-dlp" \
|
||||
--url "https://masteracnolo.github.io/Freedom-Loader-Site/" \
|
||||
--maintainer "MasterAcnolo <MasterAcnolo@users.noreply.github.com>" \
|
||||
--license "GPL-3.0-only" \
|
||||
--depends gtk3 \
|
||||
--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 into a source RPM for COPR submission
|
||||
# ------------------------------------------------------------
|
||||
echo "Building SRPM for COPR..."
|
||||
mkdir -p "$ROOT_DIR/srpm-out"
|
||||
|
||||
TMP_DIR="$ROOT_DIR/.copr-build-tmp"
|
||||
mkdir -p "$TMP_DIR"
|
||||
TMP_SPEC="$TMP_DIR/freedom-loader.spec"
|
||||
sed "s/__VERSION__/$VERSION/g" "$ROOT_DIR/package/freedom-loader.spec" > "$TMP_SPEC"
|
||||
|
||||
rpmbuild -bs "$TMP_SPEC" \
|
||||
--define "_sourcedir $ROOT_DIR/dist" \
|
||||
--define "_srcrpmdir $ROOT_DIR/srpm-out"
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
echo "SRPM built: $ROOT_DIR/srpm-out/freedom-loader-$VERSION-1*.src.rpm"
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
VERSION=$(node -p "require('$ROOT_DIR/package.json').version")
|
||||
mkdir -p "$ROOT_DIR/srpm-out"
|
||||
|
||||
TMP_DIR="$ROOT_DIR/.copr-build-tmp"
|
||||
mkdir -p "$TMP_DIR"
|
||||
TMP_SPEC="$TMP_DIR/freedom-loader.spec"
|
||||
sed "s/__VERSION__/$VERSION/g" "$ROOT_DIR/package/freedom-loader.spec" > "$TMP_SPEC"
|
||||
|
||||
rpmbuild -bs "$TMP_SPEC" \
|
||||
--define "_sourcedir $ROOT_DIR/dist" \
|
||||
--define "_srcrpmdir $ROOT_DIR/srpm-out"
|
||||
|
||||
rm -rf "$TMP_DIR"
|
||||
echo "SRPM généré : $ROOT_DIR/srpm-out/freedom-loader-$VERSION-1*.src.rpm"
|
||||
184
scripts/release.sh
Executable file
184
scripts/release.sh
Executable file
@@ -0,0 +1,184 @@
|
||||
#!/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")
|
||||
|
||||
f [[ "$VERSION" == *"-preview"* ]]; then
|
||||
echo "Error: The current version ($VERSION) is a 'preview"
|
||||
echo "Unable to launch release pipeline. Please update the package.json with a stable version.."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TAG="$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 "Unknown Argument : $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.
|
||||
|
||||
**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 "============================================"
|
||||
@@ -2,6 +2,7 @@ const { fetchInfo } = require("../services/info.services");
|
||||
const { parseVideo, parsePlaylist } = require("../helpers/parseInfo.helpers");
|
||||
const { logger } = require("../logger");
|
||||
const { isValidUrl } = require("../helpers/validation.helpers");
|
||||
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
||||
|
||||
/**
|
||||
* Handles metadata retrieval for a video or playlist.
|
||||
@@ -18,7 +19,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,10 +33,11 @@ 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
|
||||
const isPlaylistUrl = url.includes("&list") || url.includes("?list");
|
||||
// Lightweight heuristic to detect playlist URLs. It works for Youtube.
|
||||
const isPlaylistUrl = isUrlPlaylist(url);
|
||||
|
||||
logger.info(
|
||||
isPlaylistUrl
|
||||
@@ -73,11 +77,11 @@ async function infoController(req, res) {
|
||||
);
|
||||
|
||||
// Preserve original error context
|
||||
throw err;
|
||||
logger.error(err);
|
||||
}
|
||||
|
||||
} else {
|
||||
throw err;
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +90,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(
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
const path = require("path");
|
||||
|
||||
const getUserBrowser = require("./getBrowser.helpers.js");
|
||||
const {getUserBrowser} = require("./getBrowser.helpers.js");
|
||||
const { ffmpegPath, denoPath} = require("./path.helpers.js");
|
||||
const { configFeatures } = require("../../config.js");
|
||||
const { logger } = require("../logger.js");
|
||||
const config = require("../../config")
|
||||
|
||||
/**
|
||||
* Validates whether the provided codec is supported by the application.
|
||||
@@ -62,7 +63,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 +71,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 +119,27 @@ 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";
|
||||
}
|
||||
|
||||
// Add a custom signature in each file downloaded with the app. It's a small things but i found it cool :)
|
||||
if (configFeatures.addMetadata) {
|
||||
args.push(
|
||||
"--postprocessor-args",
|
||||
`ffmpeg:-metadata comment="Downloaded with Freedom Loader ${config.version}"`
|
||||
);
|
||||
}
|
||||
|
||||
args.push("-o", path.join(outputFolder, outputTemplate));
|
||||
args.push(url);
|
||||
|
||||
return args.filter(Boolean);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
const notify = require("./notify.helpers")
|
||||
const notify = require("./notify.helpers");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
/**
|
||||
@@ -22,41 +21,38 @@ const { logger } = require("../logger");
|
||||
* @returns {string} The detected browser identifier.
|
||||
*/
|
||||
function getUserBrowser() {
|
||||
//const userProfile = os.homedir();
|
||||
const firefoxPath = getFirefoxProfilePath();
|
||||
const platform = process.platform;
|
||||
let isFirefoxHere = false;
|
||||
|
||||
/**
|
||||
* Browsers supported by yt-dlp cookie extraction.
|
||||
*
|
||||
* Only Firefox is currently enabled and tested.
|
||||
* Additional browsers can be enabled in the future
|
||||
* once compatibility has been verified.
|
||||
*/
|
||||
const browsers = [
|
||||
{ name: "firefox", path: firefoxPath },
|
||||
// { name: "chrome", path: path.join(userProfile, "AppData", "Local", "Google", "Chrome", "User Data", "Default") },
|
||||
// { name: "brave", path: path.join(userProfile, "AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data", "Default") },
|
||||
// { name: "edge", path: path.join(userProfile, "AppData", "Local", "Microsoft", "Edge", "User Data", "Default") },
|
||||
// { name: "opera", path: path.join(userProfile, "AppData", "Roaming", "Opera Software", "Opera Stable") },
|
||||
// { name: "vivaldi", path: path.join(userProfile, "AppData", "Local", "Vivaldi", "User Data", "Default") },
|
||||
// { name: "safari", path: path.join(userProfile, "AppData", "Local", "Apple Computer", "Safari") },
|
||||
// { name: "whale", path: path.join(userProfile, "AppData", "Local", "Naver", "Naver Whale", "User Data", "Default") }
|
||||
if (platform === "win32") {
|
||||
const paths = [
|
||||
"C:\\Program Files\\Mozilla Firefox\\firefox.exe",
|
||||
"C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe",
|
||||
`${process.env.LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
|
||||
];
|
||||
|
||||
// Search for the first available browser profile.
|
||||
for (const browser of browsers) {
|
||||
if (browser.path && fs.existsSync(browser.path)) {
|
||||
logger.info(`Browser found: ${browser.name}`);
|
||||
return browser.name;
|
||||
}
|
||||
isFirefoxHere = paths.some(p => fs.existsSync(p));
|
||||
}
|
||||
|
||||
if (platform === "linux") {
|
||||
const paths = [
|
||||
"/usr/bin/firefox",
|
||||
"/usr/bin/firefox-esr",
|
||||
"/usr/local/bin/firefox",
|
||||
"/snap/bin/firefox",
|
||||
"/var/lib/flatpak/exports/bin/org.mozilla.firefox",
|
||||
`${os.homedir()}/.local/share/flatpak/exports/bin/org.mozilla.firefox`,
|
||||
];
|
||||
isFirefoxHere = paths.some(p => fs.existsSync(p));
|
||||
}
|
||||
|
||||
if (!isFirefoxHere) {
|
||||
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
|
||||
// No supported browser found => Notify User
|
||||
logger.warn("No supported browser found on the system");
|
||||
|
||||
// If you somehow managed to live without Firefox and need help installing it. - Don't applied to my Linux chad
|
||||
if (process.platform === "win32") {
|
||||
notify.notifyFirefoxBrowserMissing();
|
||||
} else {
|
||||
logger.info("Browser found: firefox");
|
||||
}
|
||||
|
||||
// Fallback to Firefox and let yt-dlp handle the error gracefully.
|
||||
@@ -64,39 +60,4 @@ function getUserBrowser() {
|
||||
return "firefox";
|
||||
}
|
||||
|
||||
function getFirefoxProfilePath() {
|
||||
const home = os.homedir();
|
||||
|
||||
if (process.platform === "win32") {
|
||||
return path.join(
|
||||
home,
|
||||
"AppData",
|
||||
"Roaming",
|
||||
"Mozilla",
|
||||
"Firefox",
|
||||
"Profiles"
|
||||
);
|
||||
}
|
||||
|
||||
if (process.platform === "linux") {
|
||||
return path.join(
|
||||
"usr",
|
||||
"bin",
|
||||
"firefox"
|
||||
);
|
||||
}
|
||||
|
||||
if (process.platform === "darwin") {
|
||||
return path.join(
|
||||
home,
|
||||
"Library",
|
||||
"Application Support",
|
||||
"Firefox",
|
||||
"Profiles"
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = getUserBrowser
|
||||
module.exports = {getUserBrowser};
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ const binaryPaths = {
|
||||
* - user themes are persisted in userData to survive updates
|
||||
*/
|
||||
const userThemesPath = path.join(app.getPath("userData"), "themes");
|
||||
const defaultThemesSourcePath = path.join(resourcesPath, "theme");
|
||||
const defaultThemesSourcePath = path.join("theme");
|
||||
|
||||
/**
|
||||
* Initializes user theme directory and ensures default themes exist.
|
||||
@@ -172,22 +172,32 @@ function initUserThemes() {
|
||||
for (const theme of defaultThemes) {
|
||||
const srcPath = path.join(defaultThemesSourcePath, theme);
|
||||
const destPath = path.join(userThemesPath, theme);
|
||||
const srcIsFile = !fs.statSync(srcPath).isDirectory();
|
||||
|
||||
if (fs.existsSync(destPath)) {
|
||||
const destIsDir = fs.statSync(destPath).isDirectory();
|
||||
if (srcIsFile && destIsDir) {
|
||||
getLogger().warn(`Corrupted theme entry: ${theme}, fixing...`);
|
||||
fs.rmSync(destPath, { recursive: true, force: true });
|
||||
|
||||
if (!fs.existsSync(destPath)) {
|
||||
try {
|
||||
if (fs.statSync(srcPath).isDirectory()) {
|
||||
copyDirectory(srcPath, destPath);
|
||||
fs.chmodSync(destPath, 0o777);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (srcIsFile) {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
fs.chmodSync(destPath, 0o666);
|
||||
} else {
|
||||
copyDirectory(srcPath, destPath);
|
||||
fs.chmodSync(destPath, 0o777);
|
||||
}
|
||||
getLogger().info(`Copied default theme: ${theme}`);
|
||||
} catch (copyErr) {
|
||||
getLogger().warn(`Failed to copy theme ${theme}: ${copyErr.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getLogger().warn(`Default themes source path not found: ${defaultThemesSourcePath}`);
|
||||
}
|
||||
@@ -229,24 +239,31 @@ function copyDirectory(src, dest) {
|
||||
}
|
||||
|
||||
// Runtime validation of critical binaries
|
||||
if (!userYtDlp) {
|
||||
getLogger().error("Missing yt-dlp binary");
|
||||
}
|
||||
// if (!userYtDlp) {
|
||||
// getLogger().error("Missing yt-dlp binary");
|
||||
// }
|
||||
|
||||
if (!ffmpegPath) {
|
||||
getLogger().error("Missing ffmpeg binary");
|
||||
}
|
||||
// if (!ffmpegPath) {
|
||||
// getLogger().error("Missing ffmpeg binary");
|
||||
// }
|
||||
|
||||
if (!denoPath) {
|
||||
getLogger().error("Missing deno binary");
|
||||
// if (!denoPath) {
|
||||
// getLogger().error("Missing deno binary");
|
||||
// }
|
||||
|
||||
|
||||
function validateBinaries() {
|
||||
if (!userYtDlp) getLogger().error("Missing yt-dlp binary");
|
||||
if (!ffmpegPath) getLogger().error("Missing ffmpeg binary");
|
||||
if (!denoPath) getLogger().error("Missing deno binary");
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to lazy load logger, avoid circular import
|
||||
* @returns {winston.Logger}
|
||||
* @returns {import("winston").Logger}
|
||||
*/
|
||||
function getLogger() {
|
||||
return require("../logger.js").logger;
|
||||
}
|
||||
|
||||
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes, isWindows };
|
||||
module.exports = { userYtDlp, ffmpegPath, denoPath, iconPaths, binaryPaths, resourcesPath, defaultDownloadFolder, userThemesPath, initUserThemes, isWindows, validateBinaries };
|
||||
12
server/helpers/playlist.helpers.js
Normal file
12
server/helpers/playlist.helpers.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Determines whether a URL targets a playlist.
|
||||
*
|
||||
* The detection is based on the presence of the YouTube
|
||||
* playlist query parameter (`list`).
|
||||
*
|
||||
* @param {string} url - URL to inspect.
|
||||
* @returns {boolean} True if the URL appears to be a playlist.
|
||||
*/
|
||||
export function isUrlPlaylist(url) {
|
||||
return url.includes("?list=") || url.includes("&list=") || url.includes("@");
|
||||
}
|
||||
@@ -5,6 +5,7 @@ const path = require("path");
|
||||
const os = require("os");
|
||||
const config = require("../config");
|
||||
const {isWindows} = require("./helpers/path.helpers");
|
||||
const { logSystemInfo } = require("./sysinfo");
|
||||
|
||||
// Logs folder in Windows
|
||||
const logDir = isWindows ? path.join(os.homedir(), "AppData", "Local", "FreedomLoader", "logs") : path.join(process.env.XDG_DATA_HOME || path.join(os.homedir(), ".local", "share"), "FreedomLoader", "logs");
|
||||
@@ -16,9 +17,29 @@ try {
|
||||
console.error(`Failed to create log directory: ${error.message}`);
|
||||
}
|
||||
|
||||
const logFormat = format.combine(
|
||||
/**
|
||||
* Format used by the saved file
|
||||
* @type {Format}
|
||||
*/
|
||||
const fileFormat = format.combine(
|
||||
format.timestamp({format: "YYYY-MM-DD HH:mm:ss"}),
|
||||
format.printf(({ timestamp, level, message }) => `${timestamp} | ${level.toUpperCase()} | ${message}`)
|
||||
format.errors({stack: true}), // Capture la stack trace complète des erreurs
|
||||
format.printf(({timestamp, level, message, stack}) => {
|
||||
return `${timestamp} | ${level.toUpperCase()} | ${stack || message}`;
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Formated used by the IDE/Console
|
||||
* @type {Format}
|
||||
*/
|
||||
const consoleFormat = format.combine(
|
||||
format.colorize({all: true}),
|
||||
format.timestamp({format: "HH:mm:ss"}),
|
||||
format.errors({stack: true}),
|
||||
format.printf(({timestamp, level, message, stack}) => {
|
||||
return `[${timestamp}] ${level}: ${stack || message}`;
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -32,7 +53,6 @@ const logFormat = format.combine(
|
||||
*/
|
||||
const logger = createLogger({
|
||||
level: "info",
|
||||
format: logFormat,
|
||||
transports: [
|
||||
new DailyRotateFile({
|
||||
dirname: logDir,
|
||||
@@ -40,11 +60,11 @@ const logger = createLogger({
|
||||
datePattern: "YYYY-MM-DD",
|
||||
zippedArchive: false,
|
||||
maxFiles: "7d",
|
||||
format: logFormat,
|
||||
format: fileFormat,
|
||||
options: { flags: "a" },
|
||||
}),
|
||||
new transports.Console({
|
||||
format: logFormat,
|
||||
format: consoleFormat,
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -52,16 +72,18 @@ const logger = createLogger({
|
||||
/**
|
||||
* Start Log Session
|
||||
*/
|
||||
function logSessionStart() {
|
||||
logger.info(`--- Starting session: ${new Date().toISOString()} ---`);
|
||||
function logSessionStart(logDir, downloadPath) {
|
||||
logger.info(`--- Starting session: ${new Date().toLocaleString('sv-SE')} ---`);
|
||||
logger.info("============================================================")
|
||||
logger.info(`Application Version: ${config.version}`)
|
||||
if (typeof logSystemInfo === 'function') logSystemInfo(logger, logDir, downloadPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Log Session
|
||||
*/
|
||||
function logSessionEnd() {
|
||||
logger.info(`--- Ending session: ${new Date().toISOString()} ---`);
|
||||
logger.info(`--- Ending session: ${new Date().toLocaleString('sv-SE')} ---`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -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;
|
||||
@@ -1,11 +1,17 @@
|
||||
const express = require("express");
|
||||
const path = require("path");
|
||||
const { logger, logSessionEnd } = require("./logger");
|
||||
const config = require("../config");
|
||||
|
||||
const {logger} = require("./logger");
|
||||
const { rateLimit } = require("./helpers/rateLimit.helpers");
|
||||
|
||||
const app = express();
|
||||
|
||||
/**
|
||||
* Store the state of the server
|
||||
*/
|
||||
let serverInstance = null;
|
||||
|
||||
// Middlewares
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
@@ -14,6 +20,8 @@ app.use(express.static(path.join(__dirname, "../public")));
|
||||
// Routes
|
||||
app.use("/download", require("./routes/download.route"));
|
||||
app.use("/info", require("./routes/info.route"));
|
||||
|
||||
// Interface
|
||||
app.get("/", rateLimit, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, "../public/index.html"));
|
||||
});
|
||||
@@ -28,33 +36,32 @@ app.get("/", rateLimit, (req, res) => {
|
||||
*/
|
||||
async function startServer() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = app.listen(config.applicationPort, () => {
|
||||
const serverInstance = app.listen(config.applicationPort, () => {
|
||||
logger.info(`Express server ready at http://localhost:${config.applicationPort}`);
|
||||
resolve(server);
|
||||
resolve(serverInstance);
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
serverInstance.on("error", (err) => {
|
||||
logger.error("Express server error:", err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
/**
|
||||
* Clean exit function
|
||||
* - Stop Log
|
||||
* - Stop Express Server
|
||||
* - Stop Electron App
|
||||
*/
|
||||
const gracefulExit = () => {
|
||||
logSessionEnd();
|
||||
server.close(() => {
|
||||
logger.info("Express server closed cleanly.");
|
||||
process.exit();
|
||||
});
|
||||
};
|
||||
|
||||
process.on("SIGINT", gracefulExit);
|
||||
process.on("SIGTERM", gracefulExit);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { startServer };
|
||||
/**
|
||||
* Clean exit function called by Electron
|
||||
*/
|
||||
function stopServer() {
|
||||
if (serverInstance) {
|
||||
try{
|
||||
serverInstance.close();
|
||||
logger.info("Express server closed cleanly");
|
||||
} catch (err){
|
||||
logger.error("Express server was not closed cleanly:", err)
|
||||
}
|
||||
} else {
|
||||
logger.info("Express server was already closed");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { startServer, stopServer };
|
||||
@@ -6,7 +6,8 @@ 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");
|
||||
const {isUrlPlaylist} = require("../helpers/playlist.helpers");
|
||||
|
||||
/**
|
||||
* Reference to the currently running yt-dlp process.
|
||||
@@ -18,19 +19,6 @@ const { configFeatures } = require("../../config.js");
|
||||
*/
|
||||
let currentDownloadProcess = null;
|
||||
|
||||
/**
|
||||
* Determines whether a URL targets a playlist.
|
||||
*
|
||||
* The detection is based on the presence of the YouTube
|
||||
* playlist query parameter (`list`).
|
||||
*
|
||||
* @param {string} url - URL to inspect.
|
||||
* @returns {boolean} True if the URL appears to be a playlist.
|
||||
*/
|
||||
function isPlaylistUrl(url) {
|
||||
return url.includes("?list=") || url.includes("&list=");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a dedicated download folder for a playlist.
|
||||
*
|
||||
@@ -77,7 +65,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
||||
}
|
||||
|
||||
logger.error(`Could not find available playlist folder after 1000 attempts`);
|
||||
throw new Error("Unable to create playlist folder");
|
||||
logger.error("Unable to create playlist folder");
|
||||
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to create playlist folder with title "${sanitizedTitle}": ${err.message}`);
|
||||
@@ -123,7 +111,7 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
||||
* @param {string} [options.outputFolder] - Destination folder.
|
||||
* @param {string} [options.playlistTitle] - Playlist title.
|
||||
*
|
||||
* @param {Function[]} listeners - Progress listeners.
|
||||
* @param {Function[]} progressListeners - Progress listeners.
|
||||
* @param {Function[]} speedListeners - Download speed listeners.
|
||||
* @param {Function[]} stageListeners - Processing stage listeners.
|
||||
* @param {Function[]} playlistInfoListeners - Playlist progress listeners.
|
||||
@@ -132,10 +120,13 @@ function createPlaylistFolder(basePath, playlistTitle) {
|
||||
* Resolves with the final output folder path once the
|
||||
* download has completed successfully.
|
||||
*/
|
||||
function fetchDownload(options, listeners, speedListeners, stageListeners, playlistInfoListeners) {
|
||||
function fetchDownload(options, progressListeners, 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;
|
||||
|
||||
@@ -157,9 +148,9 @@ 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);
|
||||
const isPlaylist = options.playlistTitle || isUrlPlaylist(options.url);
|
||||
|
||||
if (isPlaylist && configFeatures.createPlaylistFolders) {
|
||||
if (isPlaylist && userConfig.createPlaylistFolders) {
|
||||
try {
|
||||
const playlistName = options.playlistTitle || "Untitled Playlist";
|
||||
safeOutputFolder = createPlaylistFolder(safeOutputFolder, playlistName);
|
||||
@@ -168,11 +159,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);
|
||||
@@ -182,7 +178,7 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
|
||||
child.on("close", code => {
|
||||
currentDownloadProcess = null;
|
||||
listeners.forEach(fn => fn("done"));
|
||||
progressListeners.forEach(fn => fn("done"));
|
||||
if (code === 0) resolve(safeOutputFolder);
|
||||
else if (code === null) reject(new Error(`Download cancelled by user`));
|
||||
else reject(new Error(`YT-DLP failed with code : ${code}`));
|
||||
@@ -196,11 +192,11 @@ function fetchDownload(options, listeners, speedListeners, stageListeners, playl
|
||||
|
||||
// Progress Bar
|
||||
if (line.startsWith("[download] Destination:")) {
|
||||
listeners.forEach(fn => fn("reset"));
|
||||
progressListeners.forEach(fn => fn("reset"));
|
||||
stageListeners.forEach(fn => fn("Downloading..."));
|
||||
}
|
||||
const match = line.match(/\[download\]\s+(\d+\.\d+)%/);
|
||||
if (match) listeners.forEach(fn => fn(parseFloat(match[1])));
|
||||
if (match) progressListeners.forEach(fn => fn(parseFloat(match[1])));
|
||||
|
||||
if (line.includes("MiB/s") || line.includes("KiB/s")) {
|
||||
const match = line.match(/\d+(\.\d+)?[KM]?iB\/s/);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { execFile } = require("child_process");
|
||||
const { userYtDlp, denoPath } = require("../helpers/path.helpers");
|
||||
const getUserBrowser = require("../helpers/getBrowser.helpers");
|
||||
const {getUserBrowser} = require("../helpers/getBrowser.helpers");
|
||||
const { logger } = require("../logger");
|
||||
|
||||
/**
|
||||
@@ -61,6 +61,7 @@ function fetchInfo(url) {
|
||||
|
||||
try {
|
||||
const data = JSON.parse(stdout);
|
||||
|
||||
resolve(data);
|
||||
} catch (e) {
|
||||
logger.error(`JSON Parsing Error: ${e.message}`);
|
||||
|
||||
142
server/sysinfo.js
Normal file
142
server/sysinfo.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
const { execSync } = require("child_process");
|
||||
const { app } = require("electron");
|
||||
|
||||
/**
|
||||
* Reads /etc/os-release to get the Linux distribution name
|
||||
*/
|
||||
function getLinuxDistro() {
|
||||
try {
|
||||
const content = fs.readFileSync("/etc/os-release", "utf-8");
|
||||
const pretty = content.match(/^PRETTY_NAME="?(.+?)"?$/m);
|
||||
if (pretty) return pretty[1];
|
||||
const name = content.match(/^NAME="?(.+?)"?$/m);
|
||||
return name ? name[1] : "Unknown Linux";
|
||||
} catch {
|
||||
return "Unknown Linux";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the package-type file written by electron-builder
|
||||
* to determine how the app was distributed (rpm, snap, AppImage, deb...)
|
||||
*/
|
||||
function getPackageType() {
|
||||
try {
|
||||
const pkgTypePath = require("path").join(process.resourcesPath, "package-type");
|
||||
if (fs.existsSync(pkgTypePath)) {
|
||||
return fs.readFileSync(pkgTypePath, "utf-8").trim();
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Firefox is installed on the current platform
|
||||
*/
|
||||
function detectFirefox() {
|
||||
const platform = process.platform;
|
||||
|
||||
if (platform === "win32") {
|
||||
const paths = [
|
||||
"C:\\Program Files\\Mozilla Firefox\\firefox.exe",
|
||||
"C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe",
|
||||
`${process.env.LOCALAPPDATA}\\Mozilla Firefox\\firefox.exe`,
|
||||
];
|
||||
return paths.some(p => fs.existsSync(p));
|
||||
}
|
||||
|
||||
if (platform === "linux") {
|
||||
const paths = [
|
||||
"/usr/bin/firefox",
|
||||
"/usr/bin/firefox-esr",
|
||||
"/usr/local/bin/firefox",
|
||||
"/snap/bin/firefox",
|
||||
"/var/lib/flatpak/exports/bin/org.mozilla.firefox",
|
||||
`${os.homedir()}/.local/share/flatpak/exports/bin/org.mozilla.firefox`,
|
||||
];
|
||||
if (paths.some(p => fs.existsSync(p))) return true;
|
||||
try { execSync("which firefox", { stdio: "ignore" }); return true; } catch { return false; }
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the system info object logged at startup
|
||||
*
|
||||
* @param {string} logDir - path to the log directory
|
||||
* @param {string} downloadPath - current configured download path
|
||||
*/
|
||||
function getSystemInfo(logDir, downloadPath) {
|
||||
const platform = process.platform;
|
||||
const cpus = os.cpus();
|
||||
const totalMem = (os.totalmem() / 1024 / 1024 / 1024).toFixed(1);
|
||||
const freeMem = (os.freemem() / 1024 / 1024 / 1024).toFixed(1);
|
||||
|
||||
let platformLabel;
|
||||
if (platform === "win32") {
|
||||
platformLabel = `Windows (${os.release()})`;
|
||||
} else if (platform === "linux") {
|
||||
platformLabel = `Linux — ${getLinuxDistro()}`;
|
||||
} else {
|
||||
platformLabel = `${platform} ${os.release()} (unsupported)`;
|
||||
}
|
||||
|
||||
const info = {
|
||||
"Platform": platformLabel,
|
||||
"Architecture": os.arch(),
|
||||
"CPU": cpus.length ? `${cpus[0].model.trim()} (${cpus.length} cores)` : "Unknown",
|
||||
"Memory": `${freeMem} GB free / ${totalMem} GB total`,
|
||||
"Locale": app.getLocale(),
|
||||
"Electron": process.versions.electron,
|
||||
"Node": process.versions.node,
|
||||
"Packaged": String(app.isPackaged),
|
||||
"Package type": app.isPackaged ? getPackageType() : "dev",
|
||||
"Firefox present": detectFirefox() ? "Yes" : "No — cookie features unavailable",
|
||||
"Log directory": logDir,
|
||||
"Download path": downloadPath,
|
||||
};
|
||||
|
||||
// Linux-specific extras
|
||||
if (platform === "linux") {
|
||||
const sessionType = process.env.XDG_SESSION_TYPE || "unknown";
|
||||
const waylandDisplay = process.env.WAYLAND_DISPLAY;
|
||||
const x11Display = process.env.DISPLAY;
|
||||
|
||||
info["Display server"] = waylandDisplay
|
||||
? `Wayland (${waylandDisplay})`
|
||||
: x11Display
|
||||
? `X11 (${x11Display})`
|
||||
: sessionType !== "unknown"
|
||||
? sessionType
|
||||
: "unknown";
|
||||
|
||||
info["Desktop env"] = process.env.XDG_CURRENT_DESKTOP || process.env.DESKTOP_SESSION || "unknown";
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs system info at application startup
|
||||
*
|
||||
* @param {object} logger - winston logger instance
|
||||
* @param {string} logDir - path to the log directory
|
||||
* @param {string} downloadPath - current configured download path
|
||||
*/
|
||||
function logSystemInfo(logger, logDir, downloadPath) {
|
||||
const info = getSystemInfo(logDir, downloadPath);
|
||||
const maxKeyLen = Math.max(...Object.keys(info).map(k => k.length));
|
||||
|
||||
logger.info("=".repeat(60));
|
||||
logger.info("SYSTEM INFO");
|
||||
logger.info("=".repeat(60));
|
||||
for (const [key, val] of Object.entries(info)) {
|
||||
logger.info(` ${key.padEnd(maxKeyLen)} : ${val}`);
|
||||
}
|
||||
logger.info("=".repeat(60));
|
||||
}
|
||||
|
||||
module.exports = { logSystemInfo };
|
||||
182
test/unit/buildArgs.test.js
Normal file
182
test/unit/buildArgs.test.js
Normal file
@@ -0,0 +1,182 @@
|
||||
const path = require("path");
|
||||
|
||||
jest.mock("../../server/helpers/getBrowser.helpers.js", () =>
|
||||
jest.fn(() => "firefox")
|
||||
);
|
||||
|
||||
jest.mock("../../server/helpers/path.helpers.js", () => ({
|
||||
ffmpegPath: "/mock/ffmpeg",
|
||||
denoPath: "/mock/deno",
|
||||
}));
|
||||
|
||||
jest.mock("../../config.js", () => ({
|
||||
configFeatures: {
|
||||
autoUpdate: false,
|
||||
discordRPC: false,
|
||||
customTopBar: false,
|
||||
autoCheckInfo: false,
|
||||
addThumbnail: true,
|
||||
addMetadata: true,
|
||||
verboseLogs: false,
|
||||
autoDownloadPlaylist: false,
|
||||
customCodec: "h264",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../server/logger.js", () => ({
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { buildYtDlpArgs } = require("../../server/helpers/buildArgs.helpers");
|
||||
const { configFeatures } = require("../../config");
|
||||
const { logger } = require("../../server/logger");
|
||||
|
||||
describe("buildYtDlpArgs", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("builds default video arguments", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--cookies-from-browser");
|
||||
expect(args).toContain("firefox");
|
||||
|
||||
expect(args).toContain("--ffmpeg-location");
|
||||
expect(args).toContain("/mock/ffmpeg");
|
||||
|
||||
expect(args).toContain("--js-runtimes");
|
||||
expect(args).toContain("deno:/mock/deno");
|
||||
|
||||
expect(args).toContain("--merge-output");
|
||||
expect(args).toContain("mp4");
|
||||
|
||||
expect(args).toContain("-f");
|
||||
expect(args).toContain("bestvideo+bestaudio/best/mp4");
|
||||
|
||||
expect(args).toContain(
|
||||
path.join("/downloads", "%(title)s.%(ext)s")
|
||||
);
|
||||
|
||||
expect(args.at(-1)).toBe("https://youtube.com/watch?v=123");
|
||||
});
|
||||
|
||||
test("builds audio-only arguments", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: true,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--extract-audio");
|
||||
expect(args).toContain("--audio-format");
|
||||
expect(args).toContain("mp3");
|
||||
expect(args).toContain("bestaudio");
|
||||
|
||||
expect(args).not.toContain("--merge-output");
|
||||
});
|
||||
|
||||
test("uses requested quality", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "720",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain(
|
||||
"bestvideo[height<=720]+bestaudio/best[height<=720]/mp4"
|
||||
);
|
||||
});
|
||||
|
||||
test("falls back to best for unknown quality", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "unknown",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("best");
|
||||
});
|
||||
|
||||
test("uses --no-playlist by default", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--no-playlist");
|
||||
expect(args).not.toContain("--yes-playlist");
|
||||
});
|
||||
|
||||
test("uses --yes-playlist when enabled", () => {
|
||||
configFeatures.autoDownloadPlaylist = true;
|
||||
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--yes-playlist");
|
||||
expect(args).not.toContain("--no-playlist");
|
||||
|
||||
configFeatures.autoDownloadPlaylist = false;
|
||||
});
|
||||
|
||||
test("embeds thumbnail and metadata when enabled", () => {
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(args).toContain("--embed-thumbnail");
|
||||
expect(args).toContain("--add-metadata");
|
||||
});
|
||||
|
||||
test("logs codec validation", () => {
|
||||
buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith("Codec Valid: h264");
|
||||
});
|
||||
|
||||
test("falls back to h264 when codec is invalid", () => {
|
||||
configFeatures.customCodec = "invalid";
|
||||
|
||||
const args = buildYtDlpArgs({
|
||||
url: "https://youtube.com/watch?v=123",
|
||||
audioOnly: false,
|
||||
quality: "best",
|
||||
outputFolder: "/downloads",
|
||||
});
|
||||
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
"Codec not valid: invalid. Using default codec"
|
||||
);
|
||||
|
||||
const sortIndex = args.indexOf("-S");
|
||||
expect(args[sortIndex + 1]).toContain("vcodec:h264");
|
||||
|
||||
configFeatures.customCodec = "h264";
|
||||
});
|
||||
});
|
||||
133
test/unit/notify.helpers.test.js
Normal file
133
test/unit/notify.helpers.test.js
Normal file
@@ -0,0 +1,133 @@
|
||||
const mockShow = jest.fn();
|
||||
const mockOn = jest.fn();
|
||||
const mockOpenPath = jest.fn();
|
||||
const mockOpenExternal = jest.fn();
|
||||
|
||||
jest.mock("electron", () => ({
|
||||
Notification: jest.fn().mockImplementation(() => ({
|
||||
show: mockShow,
|
||||
on: mockOn,
|
||||
})),
|
||||
shell: {
|
||||
openPath: mockOpenPath,
|
||||
openExternal: mockOpenExternal,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../server/helpers/path.helpers", () => ({
|
||||
iconPaths: {
|
||||
confirm: "/icons/confirm.png",
|
||||
error: "/icons/error.png",
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../server/logger", () => ({
|
||||
logger: {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const { Notification, shell } = require("electron");
|
||||
|
||||
const {
|
||||
notifyDownloadFinished,
|
||||
notifyCookiesBrowserError,
|
||||
notifyFirefoxBrowserMissing,
|
||||
} = require("../../server/helpers/notify.helpers");
|
||||
|
||||
describe("notify.helpers", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("notifyDownloadFinished", () => {
|
||||
test("does nothing when notifications are disabled", () => {
|
||||
notifyDownloadFinished("/downloads", false);
|
||||
|
||||
expect(Notification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("does nothing when folder is missing", () => {
|
||||
notifyDownloadFinished(undefined, true);
|
||||
|
||||
expect(Notification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("creates and shows notification", () => {
|
||||
notifyDownloadFinished("/downloads");
|
||||
|
||||
expect(Notification).toHaveBeenCalledWith({
|
||||
title: "Freedom Loader",
|
||||
body: "Your download is complete, click here to open it.",
|
||||
icon: "/icons/confirm.png",
|
||||
});
|
||||
|
||||
expect(mockOn).toHaveBeenCalledWith(
|
||||
"click",
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("opens folder when notification is clicked", () => {
|
||||
notifyDownloadFinished("/downloads");
|
||||
|
||||
const callback = mockOn.mock.calls[0][1];
|
||||
callback();
|
||||
|
||||
expect(shell.openPath).toHaveBeenCalledWith("/downloads");
|
||||
});
|
||||
});
|
||||
|
||||
describe("notifyCookiesBrowserError", () => {
|
||||
test("creates notification", () => {
|
||||
notifyCookiesBrowserError();
|
||||
|
||||
expect(Notification).toHaveBeenCalledWith({
|
||||
title: "Cookies Error",
|
||||
body: "Unable to retrieve cookies. Please log in to your browser and click here to view the tutorial.",
|
||||
icon: "/icons/error.png",
|
||||
});
|
||||
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("opens tutorial when clicked", () => {
|
||||
notifyCookiesBrowserError();
|
||||
|
||||
const callback = mockOn.mock.calls[0][1];
|
||||
callback();
|
||||
|
||||
expect(shell.openExternal).toHaveBeenCalledWith(
|
||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("notifyFirefoxBrowserMissing", () => {
|
||||
test("creates notification", () => {
|
||||
notifyFirefoxBrowserMissing();
|
||||
|
||||
expect(Notification).toHaveBeenCalledWith({
|
||||
title: "Firefox Missing",
|
||||
body: "Firefox was not found on your system. Click here to follow the installation guide",
|
||||
icon: "/icons/error.png",
|
||||
});
|
||||
|
||||
expect(mockShow).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("opens installation guide when clicked", () => {
|
||||
notifyFirefoxBrowserMissing();
|
||||
|
||||
const callback = mockOn.mock.calls[0][1];
|
||||
callback();
|
||||
|
||||
expect(shell.openExternal).toHaveBeenCalledWith(
|
||||
"https://youtube.com/shorts/cN9f4s1Mf88?si=519QCVd_-fzJqRf1"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
BIN
theme/dark.zip
BIN
theme/dark.zip
Binary file not shown.
Reference in New Issue
Block a user