Compare commits

..

49 commits
v0.6.0 ... main

Author SHA1 Message Date
0d0
fa24bc435d YTDLP_PATH everywhere
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 19s
2025-02-23 07:55:42 +01:00
0d0
da95e2024f 0.8.0
All checks were successful
Create Release / create-release (push) Successful in 29s
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 06:37:43 +01:00
0d0
6f93574e8a Remove useless helper
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 21s
2025-02-23 06:37:13 +01:00
0d0
5d0fa4e951 update systemd unit file
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 19s
2025-02-23 06:18:50 +01:00
0d0
0d3b797ec8 Remove others
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 16s
2025-02-23 06:12:33 +01:00
0d0
284313259a 0.7.2
All checks were successful
Create Release / create-release (push) Successful in 29s
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 06:06:26 +01:00
0d0
b5cbef9de7 use path also on spawn
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 15s
2025-02-23 06:05:57 +01:00
0d0
109a668413 New deploy method
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 16s
2025-02-23 06:05:06 +01:00
0d0
631b9e44f0 update name
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 18s
2025-02-23 05:38:29 +01:00
0d0
99096f3d24 0.7.1
All checks were successful
Create Release / create-release (push) Successful in 27s
Bump deps (only minor versions) / ci (push) Successful in 19s
2025-02-23 05:26:29 +01:00
0d0
6b8779f527 better code
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 16s
2025-02-23 05:22:37 +01:00
0d0
b16b84e5a3 0.7.0
All checks were successful
Create Release / create-release (push) Successful in 29s
Bump deps (only minor versions) / ci (push) Successful in 18s
2025-02-23 05:10:22 +01:00
0d0
45b85fed1d Way better UI
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 16s
2025-02-23 05:09:56 +01:00
0d0
d523c4853f Update release pipeline names
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 04:59:09 +01:00
0d0
c7e99846b1 0.6.9
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 17s
Create Release / create-release (push) Successful in 31s
2025-02-23 04:56:21 +01:00
0d0
4b9018ecbb Forgotten package lock
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 16s
2025-02-23 04:56:00 +01:00
0d0
dcb7cfec27 Add loader and new formats
Some checks failed
Bump deps (only minor versions) / ci (push) Failing after 11s
2025-02-23 04:55:09 +01:00
0d0
204bceeee3 Add +x to downloaded yt-dlp 2025-02-23 04:54:48 +01:00
0d0
151b5432cd Update globals 2025-02-23 02:35:00 +01:00
0d0
10d8ce8a15 fix dc
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 18s
2025-02-23 02:31:18 +01:00
forgejo-bot
79c3c820a4 chore: update minor dependencies 2025-02-23 01:23:58 +00:00
0d0
15cb677910 try
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 19s
2025-02-23 02:21:20 +01:00
0d0
6e86aa7f8c maybe
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 18s
2025-02-23 02:12:19 +01:00
0d0
7c9b80917f lol
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 02:11:40 +01:00
0d0
f20b9869cc should work
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 18s
2025-02-23 02:08:08 +01:00
0d0
11b0b9f6a2 test
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 02:04:53 +01:00
0d0
7213189744 test
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 18s
2025-02-23 02:03:40 +01:00
0d0
a5e3e310b7 test
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 02:03:07 +01:00
0d0
e06d04f387 every minute
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 18s
2025-02-23 02:01:54 +01:00
0d0
4c08c6ee4a 0.6.7
All checks were successful
Create Release / create-release (push) Successful in 30s
Bump deps (only minor versions) / ci (push) Successful in 16s
2025-02-23 01:55:42 +01:00
0d0
6924e8e082 new release pipeline
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 01:55:25 +01:00
0d0
87746bdcac We were using install instead of clean install
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 16s
2025-02-23 01:40:40 +01:00
0d0
b13e27d7e5 0.6.6
All checks were successful
Create Release / create-release (push) Successful in 30s
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 01:37:04 +01:00
0d0
64e3e21621 fix
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 17s
2025-02-23 01:36:52 +01:00
0d0
51b61f53c8 0.6.5
Some checks failed
Bump deps (only minor versions) / ci (push) Successful in 16s
Create Release / create-release (push) Failing after 25s
2025-02-23 01:35:28 +01:00
0d0
96e12b90f0 no need to prepare
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 19s
2025-02-23 01:34:16 +01:00
0d0
bf92e60d2b update release pipeline
Some checks failed
Bump deps (only minor versions) / ci (push) Has been cancelled
2025-02-23 01:32:53 +01:00
0d0
6a731be767 test
Some checks failed
Bump deps (only minor versions) / ci (push) Has been cancelled
2025-02-23 01:24:57 +01:00
0d0
48bd9a40e0 prepare does not need to be done here
Some checks failed
Bump deps (only minor versions) / ci (push) Failing after 23s
2025-02-23 01:22:27 +01:00
0d0
074320ba0e test ci
Some checks failed
Bump deps (only minor versions) / ci (push) Failing after 20s
2025-02-23 01:20:00 +01:00
0d0
5a3ca7d8a8 Add node types
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 31s
2025-02-23 01:00:01 +01:00
0d0
750050f8cc 0.6.2
Some checks failed
Create Release / create-release (push) Successful in 30s
Bump deps (only minor versions) / ci (push) Failing after 19s
2025-02-23 00:56:55 +01:00
0d0
966e8ae14c the migration is complete 2025-02-23 00:56:14 +01:00
0d0
5c8c68077c the migration is completed 2025-02-23 00:54:13 +01:00
0d0
771eb0429b the migration is completed 2025-02-23 00:53:57 +01:00
0d0
0fcf0562cb fmt
All checks were successful
Bump deps (only minor versions) / ci (push) Successful in 20s
2025-02-22 23:59:50 +01:00
0d0
0ecdd76aab update to tailwind4 2025-02-22 23:59:41 +01:00
0d0
7d1cc1d1d8 some lint and format
Some checks failed
Bump deps (only minor versions) / ci (push) Failing after 20s
2025-02-22 23:51:11 +01:00
0d0
7f8b192a99 bump deps
Some checks failed
Bump deps (only minor versions) / ci (push) Failing after 20s
2025-02-22 23:50:50 +01:00
22 changed files with 1064 additions and 1385 deletions

View file

@ -1,3 +1,4 @@
YTDLP_PATH=./yt-dlp
ORIGIN=http://example.com
HOST=0.0.0.0 HOST=0.0.0.0
ORIGIN=https://dl.emersa.it
YTDLP_PATH=./yt-dlp
PUBLIC_VERSION=0.6.3

View file

@ -17,6 +17,5 @@ jobs:
node-version: '20' node-version: '20'
- name: Prepare, Check, Lint and Format - name: Prepare, Check, Lint and Format
run: | run: |
npm install npm ci
npm run prepare npm run prepare
npm run check

View file

@ -2,7 +2,7 @@ name: Bump deps (only minor versions)
on: on:
schedule: schedule:
- cron: '0 0 * * *' # Runs every night at midnight (UTC) - cron: '0 23 * * *' # Runs every night at midnight
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@ -23,7 +23,7 @@ jobs:
- name: Update deps, install them (to change package-lock.json) and commit - name: Update deps, install them (to change package-lock.json) and commit
run: | run: |
npx npm-check-updates --target minor -u npx npm-check-updates --target minor -u
npm install npm ci
git config --global user.name "forgejo-bot" git config --global user.name "forgejo-bot"
git config --global user.email "bot@pweapon.org" git config --global user.email "bot@pweapon.org"
git add package.json || exit 0 git add package.json || exit 0

View file

@ -21,14 +21,24 @@ jobs:
with: with:
node-version: '20' node-version: '20'
- name: Prepare, Check, Lint and Format - name: Install JQ for extracting package.json version
run: | run: |
npm install apt-get update
apt-get install -y jq
- name: Create Release
run: |
npm ci
touch .env
echo "HOST=0.0.0.0" >> .env
echo "ORIGIN=https://dl.emersa.it" >> .env
echo "PUBLIC_VERSION=$(cat package.json | jq .version)" >> .env
npm run build npm run build
cp .env.example build/.env
cp package* build/ cp package* build/
cp .env build/
mkdir releases mkdir releases
tar czvf releases/build.tar.gz build/ cd build/
tar czvf ../releases/build.tar.gz .
- name: Upload release - name: Upload release
uses: actions/forgejo-release@v2 uses: actions/forgejo-release@v2

View file

@ -39,3 +39,4 @@ It's a svelte(kit) frontend that uses `yt-dlp` to let the user download things f
- Containerfile for container build - Containerfile for container build
- Source spotify (spotdl) - Source spotify (spotdl)
- Parse URL info without `youtube-dl-exec` - Parse URL info without `youtube-dl-exec`
- Dockerfile inside the forgejo release action

View file

@ -5,9 +5,9 @@ After=network.target
[Service] [Service]
User=user User=user
Group=user Group=user
WorkingDirectory=<PROJECT_ROOT> WorkingDirectory=/home/user/downloader
ExecStart=/usr/bin/node <PROJECT_ROOT> EnvironmentFile=/home/user/downloader/.env
Environment=ORIGIN=http://example.com ExecStart=/usr/bin/node /home/user/downloader
Restart=always Restart=always
RestartSec=10 RestartSec=10
StandardOutput=syslog StandardOutput=syslog
@ -16,3 +16,4 @@ SyslogIdentifier=downloader
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

1997
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,13 @@
{ {
"name": "dl.emersa.it", "name": "dl.emersa.it",
"private": true, "private": true,
"version": "0.6.0", "version": "0.8.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",
"build": "vite build", "build": "vite build",
"download-yt-dlp": "wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -O yt-dlp", "download-yt-dlp": "wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -O yt-dlp ; chmod +x yt-dlp",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''", "prepare": "svelte-kit sync || echo ''",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
@ -14,31 +15,32 @@
"lint": "prettier --check . && eslint ." "lint": "prettier --check . && eslint ."
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.6", "@eslint/compat": "^1.2.7",
"@eslint/js": "^9.20.0", "@eslint/js": "^9.21.0",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.17.1", "@sveltejs/kit": "^2.17.2",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/vite": "^4.0.8",
"@tailwindcss/typography": "^0.5.16", "eslint": "^9.21.0",
"@types/node": "^22.13.4",
"autoprefixer": "^10.4.20",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.1", "eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1", "eslint-plugin-svelte": "^2.46.1",
"globals": "^15.15.0", "globals": "^16.0.0",
"prettier": "^3.5.1", "mdsvex": "^0.12.3",
"prettier": "^3.5.2",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11", "prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.20.0", "svelte": "^5.20.2",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"tailwindcss": "^3.4.17", "tailwindcss": "^4.0.8",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.24.0", "typescript-eslint": "^8.24.1",
"vite": "^6.1.0" "vite": "^6.1.1"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.0.8",
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^22.13.5",
"youtube-dl-exec": "^3.0.15" "youtube-dl-exec": "^3.0.15"
} }
} }

View file

@ -1,6 +1,5 @@
export default { export default {
plugins: { plugins: {
tailwindcss: {}, '@tailwindcss/postcss': {}
autoprefixer: {}
} }
}; };

View file

@ -2,31 +2,11 @@
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
NO_DELETE=false
SSH_SERVER="example" SSH_SERVER="example"
PROJECT_ROOT=example_directory PROJECT_ROOT=example_directory
SSH_REMOTE_DIR="${SSH_SERVER}:${PROJECT_ROOT}" SSH_REMOTE_DIR="${SSH_SERVER}:${PROJECT_ROOT}"
for arg in "$@" ssh "${SSH_SERVER}" "mkdir -p ${PROJECT_ROOT}"
do rsync "${__dir}/install.sh" "${SSH_SERVER}:${PROJECT_ROOT}/install.sh"
if [ "$arg" == "--no-delete" ]; then ssh "${SSH_SERVER}" "${PROJECT_ROOT}/install.sh ${PROJECT_ROOT}"
NO_DELETE=true
fi
done
if [ "$NO_DELETE" = false ]; then
echo "Deleting node_modules..."
rm -rf node_modules/
else
echo "Skipping deletion of node_modules."
fi
npm ci
npm run build
rsync -r --delete --progress build/ "${SSH_REMOTE_DIR}"
rsync package.json "${SSH_REMOTE_DIR}"
rsync package-lock.json "${SSH_REMOTE_DIR}"
ssh "${SSH_SERVER}" "cd ${PROJECT_ROOT}; npm ci"
ssh "${SSH_SERVER}" "systemctl restart downloader" ssh "${SSH_SERVER}" "systemctl restart downloader"

18
scripts/install.sh Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/bash
PROJECT_ROOT=$1
if [ -z ${PROJECT_ROOT+x} ]; then
echo "error no project_root set"
exit 1
fi
mkdir -p "${PROJECT_ROOT}"
cd "${PROJECT_ROOT}" || exit 1
rm -rf ./*
wget https://git.pweapon.org/odo/dl.emersa.it/releases/download/latest/build.tar.gz
tar -xvf build.tar.gz -C .
rm build.tar.gz
npm ci
npm run download-yt-dlp
echo "YTDLP_PATH=$(readlink -f yt-dlp)" >>.env

View file

@ -1,3 +1,22 @@
@import 'tailwindcss/base'; @import 'tailwindcss';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities'; @plugin '@tailwindcss/typography';
@plugin '@tailwindcss/forms';
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}

View file

@ -11,23 +11,9 @@
font-family: 'Press Start 2P'; font-family: 'Press Start 2P';
src: url('/fonts/PressStart2P-Regular.ttf'); src: url('/fonts/PressStart2P-Regular.ttf');
} }
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
body { body {
background: black; /* Fallback */ background: black; /* Fallback */
/* background-image:
radial-gradient(circle, #37ff1456 8%, transparent 8%),
radial-gradient(circle, #37ff1456 8%, transparent 8%); */
background-size:
60px 60px,
30px 30px; /* Larger grid sizes for more spacing */
background-position:
0 0,
15px 15px; /* Offset the smaller pattern slightly */
font-family: 'Press Start 2P', sans-serif; font-family: 'Press Start 2P', sans-serif;
color: #37ff1456; /* Retro green text */ color: #37ff1456; /* Retro green text */
margin: 0; margin: 0;

View file

@ -0,0 +1,6 @@
{
"mp3": "audio/mpeg",
"mp4": "video/mp4",
"opus": "audio/ogg",
"wav": "audio/wav"
}

View file

@ -1,6 +1,8 @@
<div <div
class="absolute inset-0 z-10 flex items-center justify-center bg-white bg-opacity-50" class=" backdrop-blur-xs bg-black/20 absolute inset-0 z-10 flex items-center justify-center "
id="spinner" id="spinner"
> >
<div class="h-20 w-20 animate-spin rounded-full border-b-2 border-t-2 border-gray-900"></div> <div class="w-[150px]">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><linearGradient id="a3"><stop offset="0" stop-color="#FF156D" stop-opacity="0"></stop><stop offset="1" stop-color="#FF156D"></stop></linearGradient><circle fill="none" stroke="url(#a3)" stroke-width="15" stroke-linecap="round" stroke-dasharray="0 44 0 44 0 44 0 44 0 360" cx="100" cy="100" r="70" transform-origin="center"><animateTransform type="rotate" attributeName="transform" calcMode="discrete" dur="2" values="360;324;288;252;216;180;144;108;72;36" repeatCount="indefinite"></animateTransform></circle></svg>
</div>
</div> </div>

13
src/lib/server/helpers.ts Normal file
View file

@ -0,0 +1,13 @@
import formats from '$lib/common/supportedFormats.json';
const formatMime = new Map(Object.entries(formats))
export const isURLValid = (url: string) => {
try {
new URL(url)
} catch {
return false
}
return true;
}
export const mimeTypeMap = formatMime;

View file

@ -1,6 +1,9 @@
import { create } from 'youtube-dl-exec'; import { create } from 'youtube-dl-exec';
import { env } from '$env/dynamic/private'; import { env } from '$env/dynamic/private';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import supportedFormats from '$lib/common/supportedFormats.json';
import { mimeTypeMap } from '$lib/server/helpers';
const YTDLP_PATH: string = env.YTDLP_PATH as string; const YTDLP_PATH: string = env.YTDLP_PATH as string;
export const ytdl = create(YTDLP_PATH); export const ytdl = create(YTDLP_PATH);
@ -21,20 +24,28 @@ export async function getYouTubeMetadata(link: string) {
* Streams the YouTube video/audio using youtube-dl-exec * Streams the YouTube video/audio using youtube-dl-exec
*/ */
export function streamYouTube(link: string, format: string): ReadableStream<Uint8Array> { export function streamYouTube(link: string, format: string): ReadableStream<Uint8Array> {
const mimeType: string | undefined = mimeTypeMap.get(format)
if (!mimeType) {
throw new Error("Unsupported format");
}
return new ReadableStream({ return new ReadableStream({
start(controller) { start(controller) {
const args = [ const args = [
'-o', '-o',
'-', '-',
format === 'mp3' ? '--embed-metadata' : '',
'--format',
format === 'mp3' ? 'bestaudio' : 'best',
'--audio-format',
format === 'mp3' ? 'mp3' : '',
'--no-playlist'
].filter(Boolean); ].filter(Boolean);
const process = spawn('yt-dlp', [...args, link], { stdio: ['ignore', 'pipe', 'ignore'] }); if(mimeType?.includes('audio')) {
args.push(...['--extract-audio', '--embed-metadata', '--embed-thumbnail', '--audio-format', format])
} else if (mimeType.includes('video')) {
args.push(...['--embed-metadata', '--embed-thumbnail', '--format', format])
}
console.info(`${YTDLP_PATH} ${args.join(' ')} ${link}`)
const process = spawn(YTDLP_PATH, [...args, link], { stdio: ['ignore', 'pipe', 'ignore'] });
process.stdout.on('data', (chunk) => controller.enqueue(chunk)); process.stdout.on('data', (chunk) => controller.enqueue(chunk));
process.stdout.on('end', () => controller.close()); process.stdout.on('end', () => controller.close());

View file

@ -1,34 +1,80 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_VERSION } from '$env/static/public';
import supportedFormats from '$lib/common/supportedFormats.json';
import Loader from '$lib/components/Loader.svelte';
import { onMount } from 'svelte';
let source = $state('youtube'); let source = $state('youtube');
let link = $state(''); let link = $state('');
let format = $state('mp3'); let format = $state('mp3');
let showModal = $state(false); let showModal = $state(false);
let href = $state(''); let href = $state('');
// let formats = ['ogg', 'mp3', 'mp4'] let disabled = $state(true);
const formats = [{ value: 'mp3', label: 'MP3' }]; let metadata = $state(true);
let downloading = $state(false);
const formats = Object.keys(supportedFormats).map((f) => {
return { value: f, label: f.toUpperCase() };
});
const toggleModal = () => { const toggleModal = () => {
showModal = !showModal; showModal = !showModal;
}; };
const handleSubmit = async (e: SubmitEvent) => { onMount(() => {
e.preventDefault(); document.cookie = 'downloading=0'
});
const onClick = () => {
link = '';
downloading = true;
document.cookie = 'downloading=1'
const id = setInterval(() => {
if (document.cookie.includes('downloading=0')) {
console.log("FINITO!")
downloading = false && clearInterval(id);
}
}, 1000);
};
const createAnchor = () => {
console.log({ console.log({
source, source,
link, link,
format format,
metadata
}); });
if (!(source && link && format)) {
disabled = true;
return;
}
try {
new URL(link);
disabled = false;
} catch (err) {
/*
if (err.code === 'ERR_INVALID_URL') {
}
*/
disabled = true;
return;
}
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
searchParams.append('source', source); searchParams.append('source', source);
searchParams.append('link', link); searchParams.append('link', link);
searchParams.append('format', format); searchParams.append('format', format);
searchParams.append('metadata', metadata);
href = `/download?${searchParams.toString()}`; href = `/download?${searchParams.toString()}`;
}; };
$effect(() => { $effect(() => {
createAnchor();
// Auto selected the radio button based on url regex // Auto selected the radio button based on url regex
if (link.includes('spotify')) { if (link.includes('spotify')) {
source = 'spotify'; source = 'spotify';
@ -37,15 +83,18 @@
} }
}); });
</script> </script>
<div <div
id="wrapper" id="wrapper"
class="relative mx-auto rounded-lg bg-black p-6 text-green-500 shadow-lg sm:max-w-sm sm:border-4 sm:border-green-500 md:mt-10 md:max-w-md lg:max-w-lg 2xl:max-w-2xl" class="relative mx-auto rounded-lg bg-black p-6 text-[#00ff7f] shadow-lg sm:max-w-sm sm:border-4 sm:border-[#00ff7f] md:mt-10 md:max-w-md lg:max-w-lg 2xl:max-w-2xl"
> >
<div id="loader" class={[{ downloading }]}>
<Loader />
</div>
<!-- Info Icon --> <!-- Info Icon -->
<button <button
onclick={toggleModal} onclick={toggleModal}
class="absolute right-3 top-3 text-pink-500 transition hover:text-pink-300" class="absolute top-3 right-3 text-[#ff007f] transition hover:text-[#ff3399]"
aria-label="Open Info Modal" aria-label="Open Info Modal"
> >
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="h-6 w-6" viewBox="0 0 24 24"> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="h-6 w-6" viewBox="0 0 24 24">
@ -55,11 +104,11 @@
</svg> </svg>
</button> </button>
<h1 id="title" class="mb-6 text-center text-xl">🐙 Scaricatore 🐙</h1> <h1 id="title" class="mb-6 text-center text-2xl text-[#00e5ff]">🐙 Scaricatore 🐙</h1>
<form class="space-y-6" onsubmit={handleSubmit}> <form class="space-y-6">
<!-- Source Selection --> <!-- Source Selection -->
<fieldset class="space-y-4"> <fieldset class="space-y-4">
<legend class="text-green-400">Choose Source:</legend> <legend class="text-[#00e5ff]">Choose Source:</legend>
<label class="flex items-center space-x-3"> <label class="flex items-center space-x-3">
<input type="radio" name="source" value="youtube" bind:group={source} class="retro-radio" /> <input type="radio" name="source" value="youtube" bind:group={source} class="retro-radio" />
@ -81,20 +130,23 @@
<label class="flex items-center space-x-3"> <label class="flex items-center space-x-3">
<input type="radio" name="source" value="other" bind:group={source} class="retro-radio" /> <input type="radio" name="source" value="other" bind:group={source} class="retro-radio" />
<span> <span>
Other (<a Other
<!--
(<a
href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md" href="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="text-pink-500 hover:underline" class="text-[#ff3399] hover:underline"
>supported sites >supported sites
</a>) </a>)
-->
</span> </span>
</label> </label>
</fieldset> </fieldset>
<!-- Link Input --> <!-- Link Input -->
<div> <div>
<label for="link" class="mb-2 block text-green-400"> Enter Playlist or Video Link: </label> <label for="link" class="mb-2 block text-[#00e5ff]"> Enter Playlist or Video Link: </label>
<input <input
name="link" name="link"
type="url" type="url"
@ -102,18 +154,18 @@
bind:value={link} bind:value={link}
required required
placeholder="Paste your link here" placeholder="Paste your link here"
class="w-full rounded-lg border-4 border-green-500 bg-green-200 px-4 py-3 text-black focus:border-pink-500 focus:outline-none" class="w-full rounded-lg border-4 border-[#00ff7f] bg-[#001a00] px-4 py-3 text-[#00ff7f] focus:border-[#ff3399] focus:outline-none"
/> />
</div> </div>
<!-- Format Selection --> <!-- Format Selection -->
<div> <div>
<label for="format" class="mb-2 block text-green-400"> Choose Format: </label> <label for="format" class="mb-2 block text-[#00e5ff]"> Choose Format: </label>
<select <select
id="format" id="format"
name="format" name="format"
bind:value={format} bind:value={format}
class="w-full rounded-lg border-4 border-green-500 bg-green-200 px-4 py-3 text-black focus:border-pink-500 focus:outline-none" class="w-full rounded-lg border-4 border-[#00ff7f] bg-[#001a00] px-4 py-3 text-[#00ff7f] focus:border-[#ff3399] focus:outline-none"
> >
{#each formats as format} {#each formats as format}
<option value={format.value}>{format.label}</option> <option value={format.value}>{format.label}</option>
@ -121,39 +173,50 @@
</select> </select>
</div> </div>
<!-- Submit Button --> <!-- Metadata -->
<button <div>
type="submit" <label for="metadata" class="mb-2 block text-[#00e5ff]"> Filename </label>
class="w-full rounded-lg border-4 border-pink-700 bg-pink-500 px-4 py-3 text-black transition hover:bg-pink-600 active:border-yellow-500" <input
> type="checkbox"
Create download link id="metadata"
</button> name="metadata"
bind:checked={metadata}
class="rounded-lg border-4 border-[#00ff7f] bg-[#001a00] px-4 py-3 text-[#00ff7f] focus:border-[#ff3399] focus:outline-none"
/>
</div>
{#if href} <a
<a class="download-link" {href}> Download </a> id="btn-download"
{/if} {href}
onclick={onClick}
class="{disabled
? 'disabled'
: ''} block w-full rounded-lg border-4 border-[#ff3399] bg-[#ff007f] px-4 py-3 text-center text-4xl text-black transition hover:bg-[#ff3399] active:border-yellow-500"
>
DOWNLOAD
</a>
</form> </form>
</div> </div>
<!-- Modal --> <!-- Modal -->
{#if showModal} {#if showModal}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80"> <div class="bg-opacity-80 fixed inset-0 z-50 flex items-center justify-center bg-black">
<div <div
class="w-4/5 max-w-lg rounded-lg border-4 border-green-500 bg-green-900 p-6 text-center text-green-100" class="w-4/5 max-w-lg rounded-lg border-4 border-[#00ff7f] bg-[#002b00] p-6 text-center text-[#00ff7f]"
> >
<h2 class="mb-4 text-lg">🐙 Scaricatore 🐙</h2> <h2 class="mb-4 text-lg">🐙 Scaricatore v{PUBLIC_VERSION} 🐙</h2>
<p> <p>
This app allows you to download Spotify playlists and YouTube videos directly. Choose your This app allows you to download Spotify playlists and YouTube videos directly. Choose your
source, paste the link, and select a format to start downloading! source, paste the link, and select a format to start downloading!
</p> </p>
<span class="mt-10 block"> <span class="mt-10 block">
<a class="underline" href="https://git.pweapon.org/odo/dl.emersa.it" <a class="underline text-[#00e5ff]" href="https://git.pweapon.org/odo/dl.emersa.it"
>Click here for the source code</a >Click here for the source code</a
> >
</span> </span>
<button <button
onclick={toggleModal} onclick={toggleModal}
class="mt-6 rounded-lg border-4 border-pink-700 bg-pink-500 px-4 py-2 text-black hover:bg-pink-600" class="mt-6 rounded-lg border-4 border-[#ff3399] bg-[#ff007f] px-4 py-2 text-black hover:bg-[#ff3399]"
> >
Close Close
</button> </button>
@ -163,15 +226,15 @@
<style> <style>
* { * {
font-size: 12px; font-size: 14px;
font-family: 'Press Start 2P', cursive;
} }
.download-link { #loader {
margin: 0 auto; display: none;
padding: 5px; }
#loader.downloading {
display: block; display: block;
text-decoration: underline;
text-align: center;
} }
.not-available { .not-available {
text-decoration-line: line-through; text-decoration-line: line-through;
@ -181,17 +244,17 @@
.retro-radio { .retro-radio {
appearance: none; appearance: none;
background-color: #000; background-color: #000;
border: 2px solid #39ff14; border: 2px solid #00ff7f;
width: 20px; width: 20px;
height: 20px; height: 20px;
cursor: pointer; cursor: pointer;
} }
.retro-radio:checked { .retro-radio:checked {
background-color: #39ff14; background-color: #00ff7f;
box-shadow: box-shadow:
0 0 4px #39ff14, 0 0 4px #00ff7f,
0 0 10px #39ff14; 0 0 10px #00ff7f;
} }
input[type='url'], input[type='url'],
@ -200,6 +263,16 @@
} }
#title { #title {
font-size: 22px; font-size: 24px;
}
#btn-download {
font-size: 26px;
transition: opacity 0.3s ease;
}
a.disabled {
pointer-events: none;
cursor: default;
opacity: 0.5;
} }
</style> </style>

View file

@ -1,12 +1,14 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { getYouTubeMetadata, streamYouTube } from '$lib/server/ytdlp'; import { getYouTubeMetadata, streamYouTube, ytdl } from '$lib/server/ytdlp';
import { isURLValid, mimeTypeMap } from '$lib/server/helpers';
export const GET: RequestHandler = async ({ url }) => { const validateRequest = (url: URL) => {
// Get query params // Get query params
const link = url.searchParams.get('link'); const link = url.searchParams.get('link');
const format = url.searchParams.get('format'); // mp3, mp4 const format = url.searchParams.get('format'); // mp3, mp4
const source = url.searchParams.get('source'); // youtube or spotify const source = url.searchParams.get('source'); // youtube or spotify
const metadata = url.searchParams.get('metadata');
// Validate input // Validate input
if (!link || !format || !source) { if (!link || !format || !source) {
@ -17,23 +19,50 @@ export const GET: RequestHandler = async ({ url }) => {
throw error(400, 'Currently, only YouTube is supported'); throw error(400, 'Currently, only YouTube is supported');
} }
try { if (!isURLValid(link)) {
// Fetch metadata for filename throw error(400, 'URL not valid');
const metadata = await getYouTubeMetadata(link); }
const { title, uploader } = metadata;
const safeTitle = `${uploader} - ${title}`; if (!mimeTypeMap.get(format)) {
const filename = `${safeTitle}.${format}`; throw error(400, 'format not valid');
}
return {
link, format, source, metadata
}
}
export const GET: RequestHandler = async ({ url }) => {
const { format, source, metadata, link } = validateRequest(url)
let filename = `you-clicked-no-metadata-so-i-cant-put-a-correct-name.${format}`;
if (!!metadata) {
try {
// Fetch metadata for filename
const ytMetadata = await getYouTubeMetadata(link);
const { title, uploader } = ytMetadata;
const safeTitle = `${uploader} - ${title}`;
filename = `${safeTitle}.${format}`;
} catch (err) {
console.error(err)
console.error('Error fetching metadata:');
throw error(500, 'Failed to fetch video metadata');
}
}
try {
console.log(filename);
// Stream video/audio // Stream video/audio
return new Response(streamYouTube(link, format), { return new Response(streamYouTube(link, format), {
headers: { headers: {
'Content-Type': format === 'mp3' ? 'audio/mpeg' : 'video/mp4', 'Content-Type': "text/event-stream",
'Content-Disposition': `attachment; filename="${filename}"` 'Content-Disposition': `attachment; filename="${filename}"`,
'Set-Cookie': 'downloading=0'
} }
}); });
} catch (err) { } catch (err) {
console.error('Error fetching metadata:', err); console.error(err)
throw error(500, 'Failed to fetch video metadata'); console.error('Filed to stream file');
throw error(500, 'Failed to stream file');
} }
}; };

View file

@ -1,3 +1,4 @@
import { mdsvex } from 'mdsvex';
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
@ -5,14 +6,13 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = { const config = {
// Consult https://svelte.dev/docs/kit/integrations // Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: [vitePreprocess(), mdsvex()],
kit: { kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter() adapter: adapter()
} },
extensions: ['.svelte', '.svx']
}; };
export default config; export default config;

View file

@ -1,13 +0,0 @@
import forms from '@tailwindcss/forms';
import typography from '@tailwindcss/typography';
import type { Config } from 'tailwindcss';
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: [typography, forms]
} satisfies Config;

View file

@ -1,6 +1,7 @@
import tailwindcss from '@tailwindcss/vite';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [sveltekit()] plugins: [sveltekit(), tailwindcss()]
}); });