Compare commits

..

No commits in common. "main" and "v0.6.0" have entirely different histories.
main ... v0.6.0

22 changed files with 1382 additions and 1061 deletions

View file

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

View file

@ -17,5 +17,6 @@ jobs:
node-version: '20' node-version: '20'
- name: Prepare, Check, Lint and Format - name: Prepare, Check, Lint and Format
run: | run: |
npm ci npm install
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 23 * * *' # Runs every night at midnight - cron: '0 0 * * *' # Runs every night at midnight (UTC)
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 ci npm install
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,24 +21,14 @@ jobs:
with: with:
node-version: '20' node-version: '20'
- name: Install JQ for extracting package.json version - name: Prepare, Check, Lint and Format
run: | run: |
apt-get update npm install
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
cd build/ tar czvf releases/build.tar.gz 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,4 +39,3 @@ 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=/home/user/downloader WorkingDirectory=<PROJECT_ROOT>
EnvironmentFile=/home/user/downloader/.env ExecStart=/usr/bin/node <PROJECT_ROOT>
ExecStart=/usr/bin/node /home/user/downloader Environment=ORIGIN=http://example.com
Restart=always Restart=always
RestartSec=10 RestartSec=10
StandardOutput=syslog StandardOutput=syslog
@ -16,4 +16,3 @@ SyslogIdentifier=downloader
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

1989
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,12 @@
{ {
"name": "dl.emersa.it", "name": "dl.emersa.it",
"private": true, "private": true,
"version": "0.8.0", "version": "0.6.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 ; chmod +x yt-dlp", "download-yt-dlp": "wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_linux -O 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",
@ -15,32 +14,31 @@
"lint": "prettier --check . && eslint ." "lint": "prettier --check . && eslint ."
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.7", "@eslint/compat": "^1.2.6",
"@eslint/js": "^9.21.0", "@eslint/js": "^9.20.0",
"@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-node": "^5.2.12",
"@sveltejs/kit": "^2.17.2", "@sveltejs/kit": "^2.17.1",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/vite": "^4.0.8", "@tailwindcss/forms": "^0.5.10",
"eslint": "^9.21.0", "@tailwindcss/typography": "^0.5.16",
"@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": "^16.0.0", "globals": "^15.15.0",
"mdsvex": "^0.12.3", "prettier": "^3.5.1",
"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.2", "svelte": "^5.20.0",
"svelte-check": "^4.1.4", "svelte-check": "^4.1.4",
"tailwindcss": "^4.0.8", "tailwindcss": "^3.4.17",
"typescript": "^5.7.3", "typescript": "^5.7.3",
"typescript-eslint": "^8.24.1", "typescript-eslint": "^8.24.0",
"vite": "^6.1.1" "vite": "^6.1.0"
}, },
"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,5 +1,6 @@
export default { export default {
plugins: { plugins: {
'@tailwindcss/postcss': {} tailwindcss: {},
autoprefixer: {}
} }
}; };

View file

@ -2,11 +2,31 @@
__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}"
ssh "${SSH_SERVER}" "mkdir -p ${PROJECT_ROOT}" for arg in "$@"
rsync "${__dir}/install.sh" "${SSH_SERVER}:${PROJECT_ROOT}/install.sh" do
ssh "${SSH_SERVER}" "${PROJECT_ROOT}/install.sh ${PROJECT_ROOT}" if [ "$arg" == "--no-delete" ]; then
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"

View file

@ -1,18 +0,0 @@
#!/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,22 +1,3 @@
@import 'tailwindcss'; @import 'tailwindcss/base';
@import 'tailwindcss/components';
@plugin '@tailwindcss/typography'; @import 'tailwindcss/utilities';
@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,9 +11,23 @@
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

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

View file

@ -1,8 +1,6 @@
<div <div
class=" backdrop-blur-xs bg-black/20 absolute inset-0 z-10 flex items-center justify-center " class="absolute inset-0 z-10 flex items-center justify-center bg-white bg-opacity-50"
id="spinner" id="spinner"
> >
<div class="w-[150px]"> <div class="h-20 w-20 animate-spin rounded-full border-b-2 border-t-2 border-gray-900"></div>
<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>

View file

@ -1,13 +0,0 @@
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,9 +1,6 @@
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);
@ -24,28 +21,20 @@ 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);
if(mimeType?.includes('audio')) { const process = spawn('yt-dlp', [...args, link], { stdio: ['ignore', 'pipe', 'ignore'] });
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,80 +1,34 @@
<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 disabled = $state(true); // let formats = ['ogg', 'mp3', 'mp4']
let metadata = $state(true); const formats = [{ value: 'mp3', label: 'MP3' }];
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;
}; };
onMount(() => { const handleSubmit = async (e: SubmitEvent) => {
document.cookie = 'downloading=0' e.preventDefault();
});
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';
@ -83,18 +37,15 @@
} }
}); });
</script> </script>
<div <div
id="wrapper" id="wrapper"
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" 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"
> >
<div id="loader" class={[{ downloading }]}>
<Loader />
</div>
<!-- Info Icon --> <!-- Info Icon -->
<button <button
onclick={toggleModal} onclick={toggleModal}
class="absolute top-3 right-3 text-[#ff007f] transition hover:text-[#ff3399]" class="absolute right-3 top-3 text-pink-500 transition hover:text-pink-300"
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">
@ -104,11 +55,11 @@
</svg> </svg>
</button> </button>
<h1 id="title" class="mb-6 text-center text-2xl text-[#00e5ff]">🐙 Scaricatore 🐙</h1> <h1 id="title" class="mb-6 text-center text-xl">🐙 Scaricatore 🐙</h1>
<form class="space-y-6"> <form class="space-y-6" onsubmit={handleSubmit}>
<!-- Source Selection --> <!-- Source Selection -->
<fieldset class="space-y-4"> <fieldset class="space-y-4">
<legend class="text-[#00e5ff]">Choose Source:</legend> <legend class="text-green-400">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" />
@ -130,23 +81,20 @@
<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 Other (<a
<!--
(<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-[#ff3399] hover:underline" class="text-pink-500 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-[#00e5ff]"> Enter Playlist or Video Link: </label> <label for="link" class="mb-2 block text-green-400"> Enter Playlist or Video Link: </label>
<input <input
name="link" name="link"
type="url" type="url"
@ -154,18 +102,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-[#00ff7f] bg-[#001a00] px-4 py-3 text-[#00ff7f] focus:border-[#ff3399] focus:outline-none" 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"
/> />
</div> </div>
<!-- Format Selection --> <!-- Format Selection -->
<div> <div>
<label for="format" class="mb-2 block text-[#00e5ff]"> Choose Format: </label> <label for="format" class="mb-2 block text-green-400"> 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-[#00ff7f] bg-[#001a00] px-4 py-3 text-[#00ff7f] focus:border-[#ff3399] focus:outline-none" 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"
> >
{#each formats as format} {#each formats as format}
<option value={format.value}>{format.label}</option> <option value={format.value}>{format.label}</option>
@ -173,50 +121,39 @@
</select> </select>
</div> </div>
<!-- Metadata --> <!-- Submit Button -->
<div> <button
<label for="metadata" class="mb-2 block text-[#00e5ff]"> Filename </label> type="submit"
<input 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"
type="checkbox"
id="metadata"
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>
<a
id="btn-download"
{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 Create download link
</a> </button>
{#if href}
<a class="download-link" {href}> Download </a>
{/if}
</form> </form>
</div> </div>
<!-- Modal --> <!-- Modal -->
{#if showModal} {#if showModal}
<div class="bg-opacity-80 fixed inset-0 z-50 flex items-center justify-center bg-black"> <div class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-80">
<div <div
class="w-4/5 max-w-lg rounded-lg border-4 border-[#00ff7f] bg-[#002b00] p-6 text-center text-[#00ff7f]" class="w-4/5 max-w-lg rounded-lg border-4 border-green-500 bg-green-900 p-6 text-center text-green-100"
> >
<h2 class="mb-4 text-lg">🐙 Scaricatore v{PUBLIC_VERSION} 🐙</h2> <h2 class="mb-4 text-lg">🐙 Scaricatore 🐙</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 text-[#00e5ff]" href="https://git.pweapon.org/odo/dl.emersa.it" <a class="underline" 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-[#ff3399] bg-[#ff007f] px-4 py-2 text-black hover:bg-[#ff3399]" class="mt-6 rounded-lg border-4 border-pink-700 bg-pink-500 px-4 py-2 text-black hover:bg-pink-600"
> >
Close Close
</button> </button>
@ -226,15 +163,15 @@
<style> <style>
* { * {
font-size: 14px; font-size: 12px;
font-family: 'Press Start 2P', cursive;
} }
#loader { .download-link {
display: none; margin: 0 auto;
} 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;
@ -244,17 +181,17 @@
.retro-radio { .retro-radio {
appearance: none; appearance: none;
background-color: #000; background-color: #000;
border: 2px solid #00ff7f; border: 2px solid #39ff14;
width: 20px; width: 20px;
height: 20px; height: 20px;
cursor: pointer; cursor: pointer;
} }
.retro-radio:checked { .retro-radio:checked {
background-color: #00ff7f; background-color: #39ff14;
box-shadow: box-shadow:
0 0 4px #00ff7f, 0 0 4px #39ff14,
0 0 10px #00ff7f; 0 0 10px #39ff14;
} }
input[type='url'], input[type='url'],
@ -263,16 +200,6 @@
} }
#title { #title {
font-size: 24px; font-size: 22px;
}
#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,14 +1,12 @@
import { error } from '@sveltejs/kit'; import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { getYouTubeMetadata, streamYouTube, ytdl } from '$lib/server/ytdlp'; import { getYouTubeMetadata, streamYouTube } from '$lib/server/ytdlp';
import { isURLValid, mimeTypeMap } from '$lib/server/helpers';
const validateRequest = (url: URL) => { export const GET: RequestHandler = async ({ 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) {
@ -19,50 +17,23 @@ const validateRequest = (url: URL) => {
throw error(400, 'Currently, only YouTube is supported'); throw error(400, 'Currently, only YouTube is supported');
} }
if (!isURLValid(link)) {
throw error(400, 'URL not valid');
}
if (!mimeTypeMap.get(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 { try {
// Fetch metadata for filename // Fetch metadata for filename
const ytMetadata = await getYouTubeMetadata(link); const metadata = await getYouTubeMetadata(link);
const { title, uploader } = ytMetadata; const { title, uploader } = metadata;
const safeTitle = `${uploader} - ${title}`; const safeTitle = `${uploader} - ${title}`;
filename = `${safeTitle}.${format}`; const 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': "text/event-stream", 'Content-Type': format === 'mp3' ? 'audio/mpeg' : 'video/mp4',
'Content-Disposition': `attachment; filename="${filename}"`, 'Content-Disposition': `attachment; filename="${filename}"`
'Set-Cookie': 'downloading=0'
} }
}); });
} catch (err) { } catch (err) {
console.error(err) console.error('Error fetching metadata:', err);
console.error('Filed to stream file'); throw error(500, 'Failed to fetch video metadata');
throw error(500, 'Failed to stream file');
} }
}; };

View file

@ -1,4 +1,3 @@
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';
@ -6,13 +5,14 @@ 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(), mdsvex()], preprocess: vitePreprocess(),
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;

13
tailwind.config.ts Normal file
View file

@ -0,0 +1,13 @@
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,7 +1,6 @@
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(), tailwindcss()] plugins: [sveltekit()]
}); });