Typescript remake
parent
94ba9a5a65
commit
35bcbe9568
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.idea
|
||||||
|
dist
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
# Build stage
|
||||||
|
FROM node:lts-alpine AS builder
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# copy configs folder
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
# copy source code to /app/src folder
|
||||||
|
COPY src src
|
||||||
|
|
||||||
|
# install dependencies (https://docs.npmjs.com/cli/v7/commands/npm-ci)
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:lts-alpine
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY --from=builder /app/dist dist
|
||||||
|
|
||||||
|
RUN mkdir targets
|
||||||
|
|
||||||
|
# install production dependencies
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
CMD ["npm", "start"]
|
||||||
@ -1 +1,29 @@
|
|||||||
# web-youtube-downloader
|
# Web YouTube Downloader
|
||||||
|
A simple express app which allows you to view all source urls of a YouTube video to directly download it from Google's servers.
|
||||||
|
|
||||||
|
## 📦 Dependencies
|
||||||
|
- [Express](https://expressjs.com/)
|
||||||
|
- [Typescript](https://www.typescriptlang.org/)
|
||||||
|
- [Pug](https://pugjs.org/)
|
||||||
|
- [ytdl-core](https://www.npmjs.com/package/ytdl-core)
|
||||||
|
|
||||||
|
## 💽 Installation
|
||||||
|
### Docker
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/Feuerhamster/web-youtube-downloader.git
|
||||||
|
cd web-youtube-downloader
|
||||||
|
docker build -t Feuerhamster/web-youtube-downloader .
|
||||||
|
docker run Feuerhamster/web-youtube-downloader
|
||||||
|
-e PORT=3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manually
|
||||||
|
*Requires NodeJS 14 or higher*
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/Feuerhamster/web-youtube-downloader.git
|
||||||
|
cd web-youtube-downloader
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
@ -1,55 +0,0 @@
|
|||||||
<?php
|
|
||||||
header("Content-Type: application/json");
|
|
||||||
header("Access-Control-Allow-Origin: *");
|
|
||||||
|
|
||||||
if(isset($_GET["vid"])){
|
|
||||||
|
|
||||||
|
|
||||||
$ytpage = file_get_contents("https://www.youtube.com/watch?v=".$_GET["vid"]);
|
|
||||||
|
|
||||||
unset($_COOKIE);
|
|
||||||
|
|
||||||
$pattern = "/ytplayer.config\ \=\ ({.+}});/";
|
|
||||||
|
|
||||||
preg_match_all($pattern, $ytpage, $matches);
|
|
||||||
$json = $matches[1][0];
|
|
||||||
|
|
||||||
//parse informations
|
|
||||||
|
|
||||||
$json = json_decode($json);
|
|
||||||
|
|
||||||
$ytdata = $json->args->player_response;
|
|
||||||
|
|
||||||
$ytdata = json_decode($ytdata);
|
|
||||||
|
|
||||||
if($ytdata->videoDetails->useCipher == false){
|
|
||||||
|
|
||||||
$data = new stdClass();
|
|
||||||
|
|
||||||
$data->title = $ytdata->videoDetails->title;
|
|
||||||
$data->channel = $ytdata->videoDetails->author;
|
|
||||||
$data->videoId = $ytdata->videoDetails->videoId;
|
|
||||||
$data->length = $ytdata->videoDetails->lengthSeconds;
|
|
||||||
$data->views = $ytdata->videoDetails->viewCount;
|
|
||||||
|
|
||||||
$data->sources = $ytdata->streamingData;
|
|
||||||
$data->thumbnails = $ytdata->videoDetails->thumbnail->thumbnails;
|
|
||||||
|
|
||||||
$send = json_encode($data);
|
|
||||||
|
|
||||||
echo $send;
|
|
||||||
|
|
||||||
}else{
|
|
||||||
$data = new stdClass();
|
|
||||||
$data->error = "cipher_video";
|
|
||||||
$send = json_encode($data);
|
|
||||||
echo $send;
|
|
||||||
}
|
|
||||||
|
|
||||||
}else{
|
|
||||||
$data = new stdClass();
|
|
||||||
$data->error = "missing_video_id";
|
|
||||||
$send = json_encode($data);
|
|
||||||
echo $send;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
<h1>API for Developers</h1>
|
|
||||||
<p>You can get the data of a video very easily by using our API.</p>
|
|
||||||
|
|
||||||
<h2>Web Request structure</h2>
|
|
||||||
<span class="label"><span class="label-name" style="width: 70px;display: inline-block;">Method</span><span class="label-value label-green">GET</span></span>
|
|
||||||
<span class="label"><span class="label-name" style="width: 70px;display: inline-block;">Url</span><span class="label-value label-blue">https://feuerhamster.code-elite.net/web-youtube-downloader/api.php</span></span>
|
|
||||||
<span class="label"><span class="label-name" style="width: 70px;display: inline-block;">Params</span><span class="label-value label-purble">vid={YOUTUBE VIDEO ID}</span></span>
|
|
||||||
<span class="label"><span class="label-name" style="width: 70px;display: inline-block;">Return</span><span class="label-value label-orange">JSON</span></span>
|
|
||||||
|
|
||||||
<h2 style="margin-top: 40px;">Return values</h2>
|
|
||||||
<p>There are mulitple return values for that request. The return values are also in JSON format</p>
|
|
||||||
<div class="pre">Errors:
|
|
||||||
- error: missing_video_id //returns if you have not send the video Id in the query string
|
|
||||||
- error: cipher_video" //returns if the video is protected and can not be read by the api
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pre">Video Data JSON structure:
|
|
||||||
|
|
||||||
- title
|
|
||||||
- channel
|
|
||||||
- length
|
|
||||||
- views
|
|
||||||
|
|
||||||
- sources:
|
|
||||||
|
|
||||||
- expiresInSeconds
|
|
||||||
|
|
||||||
- formats:
|
|
||||||
ARRAY()
|
|
||||||
|
|
||||||
- adaptiveFormats:
|
|
||||||
ARRAY()
|
|
||||||
|
|
||||||
- thumbnails:
|
|
||||||
ARRAY()
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<h2 style="margin-top: 40px;">Examples</h2>
|
|
||||||
<p>An example url for an api call</p>
|
|
||||||
<div class="pre">https://feuerhamster.code-elite.net/web-youtube-downloader/api.php?vid=nD6_aQIJgW0
|
|
||||||
</div class="pre">
|
|
||||||
|
|
||||||
<p>An example with Ajax in JavaScript</p>
|
|
||||||
<div class="pre">var videoId = "nD6_aQIJgW0";
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: 'GET',
|
|
||||||
url: 'https://feuerhamster.code-elite.net/web-youtube-downloader/api.php',
|
|
||||||
data: 'vid=' + videoId,
|
|
||||||
success: function(res){
|
|
||||||
|
|
||||||
if(!res.error){
|
|
||||||
console.log(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</div>
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
<div class="box box-mobile">
|
|
||||||
|
|
||||||
<img id="video-thumbnail" style="max-height: 160px;">
|
|
||||||
<div>
|
|
||||||
<h1 id="video-title" style="margin: 0px 0px 0px 20px;">...</h1>
|
|
||||||
<h2 id="video-time" style="margin: 0px 0px 0px 20px; color: #9e4b8b;">...</h2>
|
|
||||||
<h2 id="video-views" style="margin: 0px 0px 0px 20px; color: #9e4b8b;">...</h2>
|
|
||||||
<h2 id="video-channel" style="margin: 0px 0px 0px 20px; color: rgb(120,120,120)">...</h2>
|
|
||||||
<h3 id="video-links" style="margin: 0px 0px 0px 20px;">...</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>Main Sources</h1>
|
|
||||||
|
|
||||||
<div id="video-main-source">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>Adaptive Sources</h1>
|
|
||||||
|
|
||||||
<div id="video-sources">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h1>Thumbnails</h1>
|
|
||||||
|
|
||||||
<div id="video-thumbnails">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.5 KiB |
@ -1,9 +0,0 @@
|
|||||||
<div id="home-page">
|
|
||||||
|
|
||||||
<h1 style="font-size: 42px">Download YouTube videos from source!</h1>
|
|
||||||
|
|
||||||
<h2 style="margin-bottom: 50px;">All Video and Audio Tracks + Thumbnails</h2>
|
|
||||||
|
|
||||||
<input type="text" id="home-video-url" placeholder="Paste your Video URL" onchange="loadVideoURL(this.value);" />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
//get query string
|
|
||||||
var urlQs = document.URL.split('?');
|
|
||||||
|
|
||||||
//check if any querystring is set
|
|
||||||
if(urlQs[1]){
|
|
||||||
|
|
||||||
//parse querytring args
|
|
||||||
var qsArgs = urlQs[1].split('=');
|
|
||||||
|
|
||||||
if(qsArgs[0] == "vid"){
|
|
||||||
|
|
||||||
$('#content').load('./downloader.html');
|
|
||||||
getVideoData(qsArgs[1]);
|
|
||||||
|
|
||||||
}else if(qsArgs[0] == "page"){
|
|
||||||
if(qsArgs[1] == "infos"){
|
|
||||||
$('#content').load('./infos.html');
|
|
||||||
}else if(qsArgs[1] == "documentation"){
|
|
||||||
$('#content').load('./docs.php');
|
|
||||||
}else{
|
|
||||||
$('#content').html('<center><h1>ERROR 404: Page not found!</h1></center>');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}else{
|
|
||||||
$('#content').load('./home.html');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function parseMimeType(mimeType){
|
|
||||||
var regex = /(\w+)\/(\w+)\;\ codecs\=\"(.+)\"/;
|
|
||||||
var result = regex.exec(mimeType);
|
|
||||||
return { type: result[1], format: result[2], codec: result[3] };
|
|
||||||
}
|
|
||||||
|
|
||||||
function secToMin(time) {
|
|
||||||
var hr = ~~(time / 3600);
|
|
||||||
var min = ~~((time % 3600) / 60);
|
|
||||||
var sec = time % 60;
|
|
||||||
var sec_min = "";
|
|
||||||
if (hr > 0) {
|
|
||||||
sec_min += "" + hrs + ":" + (min < 10 ? "0" : "");
|
|
||||||
}
|
|
||||||
sec_min += "" + min + ":" + (sec < 10 ? "0" : "");
|
|
||||||
sec_min += "" + sec;
|
|
||||||
return sec_min+ " minutes";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVideoData(videoId){
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
type: "GET",
|
|
||||||
url: "./api.php",
|
|
||||||
data: "vid=" + videoId,
|
|
||||||
success: function(res){
|
|
||||||
|
|
||||||
console.log(res);
|
|
||||||
|
|
||||||
if(!res.error){
|
|
||||||
|
|
||||||
$('#video-title').html(res.title);
|
|
||||||
$('#video-channel').html(res.channel);
|
|
||||||
$('#video-views').html(res.views.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,') + " views");
|
|
||||||
$('#video-time').html(secToMin(res.length));
|
|
||||||
$('#video-thumbnail').attr("src", res.thumbnails[3].url);
|
|
||||||
|
|
||||||
$('#video-links').html('<a href="https://www.youtube.com/watch?v=' + res.videoId + '" style="color: #2980b9" target="_blank">https://www.youtube.com/watch?v=' + res.videoId + '<a>');
|
|
||||||
|
|
||||||
res.sources.formats.forEach(element => {
|
|
||||||
|
|
||||||
var mainMime = parseMimeType(element.mimeType);
|
|
||||||
$('#video-main-source').append('<div class="box box-mobile"> <video src="' + element.url + '" controls height="190px"></video> <div style="padding: 10px 20px 10px 20px"> <h2 style="margin-top: 0px; margin-bottom: 10px; font-weight: 100"><b style="font-weight: 700">Quality:</b> ' +element.qualityLabel+ ' </h2> <h2 style="margin-top: 0px; margin-bottom: 10px; font-weight: 100"><b style="font-weight: 700">Format:</b> ' + mainMime.format + ' </h2> <h2 style="margin-top: 0px; margin-bottom: 10px; font-weight: 100"><b style="font-weight: 700">Codec:</b> ' + mainMime.codec + ' </h2> <a href="' + element.url + '" target="_blank" class="download-link-large"><i class="fas fa-external-link-alt"></i> Download</a> </div></div>');
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
res.sources.adaptiveFormats.forEach(element => {
|
|
||||||
var mime = parseMimeType(element.mimeType);
|
|
||||||
if(mime.type == "video"){
|
|
||||||
$('#video-sources').append('<div class="box" style="margin: 10px 0px 10px 0px;"><span class="label"><span class="label-name">Type</span><span class="label-value label-red">' + mime.type + '</span></span><span class="label"><span class="label-name">Quality</span><span class="label-value label-green">' + element.qualityLabel + '</span></span><span class="label"><span class="label-name">Format</span><span class="label-value label-orange">' + mime.format + '</span></span><span class="label"><span class="label-name">Codec</span><span class="label-value label-purble">' + mime.codec + '</span></span> <div class="spacer"></div> <a href="' + element.url + '" target="_blank" style="color: rgb(50,50,50); font-size: 26px;"><i class="fas fa-external-link-alt"></i></a> </div>');
|
|
||||||
}else{
|
|
||||||
$('#video-sources').append('<div class="box" style="margin: 10px 0px 10px 0px;"><span class="label"><span class="label-name">Type</span><span class="label-value label-blue">' + mime.type + '</span></span><span class="label"><span class="label-name">Bitrate</span><span class="label-value label-green">' + element.bitrate + '</span></span><span class="label"><span class="label-name">Format</span><span class="label-value label-orange">' + mime.format + '</span></span><span class="label"><span class="label-name">Codec</span><span class="label-value label-purble">' + mime.codec + '</span></span> <div class="spacer"></div> <a href="' + element.url + '" target="_blank" style="color: rgb(50,50,50); font-size: 26px;"><i class="fas fa-external-link-alt"></i></a> </div>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.thumbnails.forEach(element => {
|
|
||||||
$('#video-thumbnails').append('<div class="box box-mobile" style="margin: 10px 0px 10px 0px; flex-direction: row"><img src="' + element.url + '" style="width: 200px;" /><div style="padding: 10px 20px 10px 20px"><span class="label" style="margin: 0px;"><span class="label-name">Size</span><span class="label-value label-green">' + element.width + 'px*' + element.height + 'px</span></span><br/><a href="' + element.url + '" target="_blank" class="download-link-large" style="font-size: 20px;" download="' + element.url + '"><i class="fas fa-external-link-alt"></i> Download</a></div></div>')
|
|
||||||
});
|
|
||||||
|
|
||||||
}else{
|
|
||||||
$('#content').html('<center><h1>An error has occurred!</h1><h2>Please try another video or try again later</h2></center>');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadVideoURL(source){
|
|
||||||
|
|
||||||
//parse soruce and get id
|
|
||||||
var ytidRegex = /(youtu\.be\/|youtube\.com\/(watch\?(.*&)?v=|(embed|v)\/))([^\?&"'>]+)/;
|
|
||||||
var videoId = ytidRegex.exec(source)[5];
|
|
||||||
|
|
||||||
window.location.href = window.location.protocol + "//" + window.location.hostname + window.location.pathname + "?vid=" + videoId;
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
|
|
||||||
<script
|
|
||||||
src="https://code.jquery.com/jquery-3.4.1.min.js"
|
|
||||||
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
|
|
||||||
crossorigin="anonymous"></script>
|
|
||||||
|
|
||||||
<title>Web-YTDL</title>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<meta name="viewport" content="width=400, initial-scale=0.8">
|
|
||||||
<meta name="description" content="Download YouTube videos from source!">
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="style.css?v=2" />
|
|
||||||
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">
|
|
||||||
<link href="https://fonts.googleapis.com/css?family=Muli&display=swap" rel="stylesheet">
|
|
||||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
|
||||||
<link rel="shortcut icon" href="./favicon.ico" type="image/x-icon">
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="head-bar">
|
|
||||||
|
|
||||||
<h1 onclick="window.location.href='./';" style="cursor: pointer;"> <i class="fas fa-file-video"></i> <span id="head-text">Web-Youtube-Downloader</span></h1>
|
|
||||||
|
|
||||||
<div class="spacer"></div>
|
|
||||||
|
|
||||||
<input type="text" id="headbar-video-url" placeholder="Paste your video URL" onchange="loadVideoURL(this.value); this.value = '';" />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="content">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<a href="?page=documentation">Developer API</a> <a href="https://github.com/Feuerhamster/web-youtube-downloader" target="_blank">GitHub</a> <a href="?page=infos">Disclaimer/Informations</a> <a href="https://hamsterlabs.de" target="_blank">Visit HamsterLabs</a>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
<script src="index.js"></script>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<h1>Disclaimer</h1>
|
|
||||||
<p>This Website is not a part of youtube or google</p>
|
|
||||||
|
|
||||||
<h1>Infos</h1>
|
|
||||||
<p>
|
|
||||||
<b>Contact the owner:</b> <br/><br/>
|
|
||||||
<b>Email: </b> myhamstermail@gmail.com<br/>
|
|
||||||
<b>GitHub: </b> github.com/Feuerhamster<br/>
|
|
||||||
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<b>Where do we have the data of the youtube videos?</b><br/><br/>
|
|
||||||
We use the official data from the youtube.com website. All URLs and metadata come from the respective page of the corresponding Youtube video.<br/><br/>
|
|
||||||
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<b>Is the downloading of youtube videos lawful?</b><br/><br/>
|
|
||||||
Everyone is allowed to download public Youtube videos for private use.<br/><br/>
|
|
||||||
|
|
||||||
<br/><br/>
|
|
||||||
|
|
||||||
<b>Does this website violate laws and regulations such as once "convert2mp3"?</b><br/><br/>
|
|
||||||
No. On "convert2mp3" the videos were converted on the servers of the provider and made available for download.<br/>
|
|
||||||
On our website, you download the videos directly from the sources on the google server.
|
|
||||||
|
|
||||||
</p>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "web-youtube-downloader",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"description": "Download YouTube videos from source",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"build": "tsc"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/Feuerhamster/web-youtube-downloader.git"
|
||||||
|
},
|
||||||
|
"author": "Feuerhamster",
|
||||||
|
"license": "ISC",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/Feuerhamster/web-youtube-downloader/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/Feuerhamster/web-youtube-downloader#readme",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
|
"pug": "^3.0.2",
|
||||||
|
"typescript": "^4.2.3",
|
||||||
|
"ytdl-core": "^4.8.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* Main styles
|
||||||
|
*/
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
min-height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: #222;
|
||||||
|
background-color: #fafafa;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* titlebox
|
||||||
|
*/
|
||||||
|
.title-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box > img {
|
||||||
|
max-height: 100px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-right: 30px;
|
||||||
|
box-shadow: 1px 1px 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box a {
|
||||||
|
margin: 5px;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #2481e6;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-box a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* responsive element
|
||||||
|
*/
|
||||||
|
.responsive {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive > h1 {
|
||||||
|
font-size: 32px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive > h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.responsive input {
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 18px;
|
||||||
|
outline: 0;
|
||||||
|
margin-right: 10px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #eb6161;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 18px;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #c32e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
max-width: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Table
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
td, th {
|
||||||
|
border-bottom: 2px solid #ddd;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:last-child > td {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr:hover td {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
td a {
|
||||||
|
color: #3488ee;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
td a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
td img {
|
||||||
|
max-height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Format label
|
||||||
|
*/
|
||||||
|
.format {
|
||||||
|
color: white;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-0 {
|
||||||
|
background-color: #43c043;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-1 {
|
||||||
|
background-color: #3488ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-2 {
|
||||||
|
background-color: #EE6565;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.mp4 {
|
||||||
|
color: #f38a00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.webm {
|
||||||
|
color: #3488ee;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Iframe
|
||||||
|
*/
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
height: 355px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 720px) {
|
||||||
|
.responsive {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
table.formats th:nth-child(5), table.formats td:nth-child(5) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
import express, { Application } from "express";
|
||||||
|
import defaultRoute from "./routes/default";
|
||||||
|
|
||||||
|
const app: Application = express();
|
||||||
|
const port: number = parseInt(process.env.PORT) || 4131;
|
||||||
|
|
||||||
|
app.set("views", "views");
|
||||||
|
app.set("view engine", "pug");
|
||||||
|
|
||||||
|
app.use(express.static("public"));
|
||||||
|
app.use(defaultRoute);
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log("App started on port: " + port);
|
||||||
|
});
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
import {Request, Response, Router} from "express";
|
||||||
|
import {VideoData} from "../types/youtube";
|
||||||
|
import {YouTube} from "../services/youtube";
|
||||||
|
|
||||||
|
const router: Router = Router();
|
||||||
|
|
||||||
|
router.get("/", async (req: Request, res: Response) => {
|
||||||
|
|
||||||
|
// No video searched -> startpage
|
||||||
|
if(!req.query.url) {
|
||||||
|
res.render("index");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let video: VideoData = await YouTube.getVideoInfo(req.query.url);
|
||||||
|
|
||||||
|
// Cant get video -> error page
|
||||||
|
if(!video) {
|
||||||
|
res.render("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render("video", video);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get("/about", (req, res) => res.render("about"));
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import ytdl, {videoInfo} from "ytdl-core";
|
||||||
|
import {FormatType, VideoData} from "../types/youtube";
|
||||||
|
import NodeCache from "node-cache";
|
||||||
|
|
||||||
|
export class YouTube {
|
||||||
|
|
||||||
|
private static cache = new NodeCache({ stdTTL: 3600 });
|
||||||
|
|
||||||
|
static async getVideoInfo(url): Promise<VideoData> {
|
||||||
|
|
||||||
|
if(this.cache.has(url)) {
|
||||||
|
return this.cache.get(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let video: videoInfo = null;
|
||||||
|
let videoData: VideoData = <VideoData>{};
|
||||||
|
|
||||||
|
// Get video info
|
||||||
|
try {
|
||||||
|
video = await ytdl.getInfo(url.toString());
|
||||||
|
} catch(e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// format info data
|
||||||
|
videoData.id = video.videoDetails.videoId;
|
||||||
|
videoData.url = video.videoDetails.video_url;
|
||||||
|
videoData.title = video.videoDetails.title;
|
||||||
|
videoData.author = video.videoDetails.author.name;
|
||||||
|
|
||||||
|
videoData.thumbnails = video.videoDetails.thumbnails.map((t) => ({
|
||||||
|
url: t.url,
|
||||||
|
size: `${t.width}x${t.height}`
|
||||||
|
}));
|
||||||
|
|
||||||
|
videoData.formats = video.formats.map((f) => ({
|
||||||
|
type: f.hasVideo && f.hasAudio ? FormatType.main : f.hasVideo ? FormatType.video : FormatType.audio,
|
||||||
|
quality: f.qualityLabel,
|
||||||
|
container: f.container,
|
||||||
|
codecs: f.codecs,
|
||||||
|
url: f.url,
|
||||||
|
bitrate: f.bitrate.toString().replace(/\B(?=(\d{3})+(?!\d))/g, " ")
|
||||||
|
}));
|
||||||
|
|
||||||
|
videoData.formats = videoData.formats.sort((a, b) => a.type < b.type ? -1 : a.type > b.type ? 1 : 0);
|
||||||
|
|
||||||
|
// Save to cache
|
||||||
|
if(!this.cache.has(url)) {
|
||||||
|
this.cache.set(url, videoData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return videoData;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
export interface VideoData {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
url: string;
|
||||||
|
thumbnails: Thumbnail[];
|
||||||
|
formats: Format[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Thumbnail {
|
||||||
|
url: string;
|
||||||
|
size: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Format {
|
||||||
|
type: FormatType
|
||||||
|
quality: string;
|
||||||
|
container: string;
|
||||||
|
codecs: string;
|
||||||
|
url: string;
|
||||||
|
bitrate: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum FormatType {
|
||||||
|
main, audio, video
|
||||||
|
}
|
||||||
@ -1,183 +0,0 @@
|
|||||||
body, html{
|
|
||||||
margin: 0px;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
font-family: 'Muli', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
*{
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4, h5, h6, p{
|
|
||||||
color: rgb(50,50,50);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width:901px){
|
|
||||||
.box-mobile{
|
|
||||||
flex-direction: column!important;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.box{
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
#head-text{
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pre{
|
|
||||||
background-color: rgba(240,240,240);
|
|
||||||
border: 2px solid rgba(230,230,230);
|
|
||||||
border-radius: 2px;
|
|
||||||
padding: 10px;
|
|
||||||
font-family: 'Courier New', Courier, monospace;
|
|
||||||
color: rgb(50,50,50);
|
|
||||||
overflow-x: auto;
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
|
|
||||||
#head-bar{
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
background-color: #d63030;
|
|
||||||
padding: 5px;
|
|
||||||
box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.3);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#head-bar > h1{
|
|
||||||
color: white;
|
|
||||||
margin: 0px;
|
|
||||||
font-family: 'Muli', sans-serif;
|
|
||||||
font-size: 28px;
|
|
||||||
padding: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spacer{
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#head-bar > input{
|
|
||||||
background-color: rgba(255,255,255,0.3);
|
|
||||||
border: 0;
|
|
||||||
padding: 8px;
|
|
||||||
margin: 2px;
|
|
||||||
font-size: 16px;
|
|
||||||
color: white;
|
|
||||||
font-family: 'Muli', sans-serif;
|
|
||||||
min-width: 300px;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#head-bar > input:focus{
|
|
||||||
box-shadow: 0px 0px 5px 1px rgba(70,70,70,0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#content{
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
min-height: calc(100% - 93px);
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: rgb(250,250,250);
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#home-page{
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
#home-page > input{
|
|
||||||
background-color: #f04f4f;
|
|
||||||
border: 0;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 5px;
|
|
||||||
font-size: 18px;
|
|
||||||
font-family: 'Muli', sans-serif;
|
|
||||||
min-width: 400px;
|
|
||||||
border-radius: 2px;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
#home-page > input:focus{
|
|
||||||
box-shadow: 0px 0px 2px 1px rgba(180,180,180,1);
|
|
||||||
}
|
|
||||||
|
|
||||||
footer{
|
|
||||||
padding: 10px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
background-color: rgb(250,250,250);
|
|
||||||
}
|
|
||||||
footer > a{
|
|
||||||
font-size: 18px;
|
|
||||||
margin-left: 10px;
|
|
||||||
margin-right: 10px;
|
|
||||||
color: rgb(100, 100, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
footer > a:hover{
|
|
||||||
color: rgb(70, 70, 70);
|
|
||||||
}
|
|
||||||
|
|
||||||
.box{
|
|
||||||
background-color: white;
|
|
||||||
padding: 10px;
|
|
||||||
box-shadow: 0px 0px 4px 1px rgba(0,0,0,0.2);
|
|
||||||
display: flex;
|
|
||||||
margin: 10px 0px 10px 0px;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label{
|
|
||||||
display: inline-block;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
.label > .label-name{
|
|
||||||
background-color: rgb(120,120,120);
|
|
||||||
color: white;
|
|
||||||
padding: 2px 5px 2px 5px;
|
|
||||||
border-radius: 2px 0px 0px 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label-value{
|
|
||||||
color: white;
|
|
||||||
padding: 2px 5px 2px 5px;
|
|
||||||
border-radius: 0px 2px 2px 0px;
|
|
||||||
}
|
|
||||||
.label > .label-blue{
|
|
||||||
background-color: #3498db;
|
|
||||||
}
|
|
||||||
.label > .label-red{
|
|
||||||
background-color: #e74c3c;
|
|
||||||
}
|
|
||||||
.label > .label-green{
|
|
||||||
background-color: #27ae60;
|
|
||||||
}
|
|
||||||
.label > .label-orange{
|
|
||||||
background-color: #e49517;
|
|
||||||
}
|
|
||||||
.label > .label-purble{
|
|
||||||
background-color: #8a52a0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download-link-large{
|
|
||||||
color: white;
|
|
||||||
font-size: 24px;
|
|
||||||
text-decoration: none;
|
|
||||||
background-color: #0984e3;
|
|
||||||
padding: 5px 10px 5px 10px;
|
|
||||||
border-radius: 2px;
|
|
||||||
box-shadow: 0px 0px 4px 1px rgba(0,0,0,0.2);
|
|
||||||
margin-top: 10px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.download-link-large:hover{
|
|
||||||
background-color: #0569b6;
|
|
||||||
}
|
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"sourceMap": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
doctype html
|
||||||
|
html(lang="en")
|
||||||
|
head
|
||||||
|
title Web YouTube Downloader
|
||||||
|
meta(name="description" content="Download YouTube videos from source!")
|
||||||
|
meta(charset="utf-8")
|
||||||
|
meta(name="viewport" content="width=device-width, initial-scale=1.0")
|
||||||
|
link(rel="stylesheet" href="style.css")
|
||||||
|
body
|
||||||
|
div.responsive
|
||||||
|
include partials/titlebox
|
||||||
|
block content
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
extends _layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
h1 About
|
||||||
|
h2 Disclaimer
|
||||||
|
p This Website is not a part of YouTube or Google.
|
||||||
|
|
||||||
|
h2 Where do we have the data of the youtube videos?
|
||||||
|
p We use the official data from the youtube.com website. All URLs and metadata come from the respective page of the corresponding Youtube video.
|
||||||
|
|
||||||
|
h2 Is the downloading of youtube videos lawful?
|
||||||
|
p Everyone is allowed to download public Youtube videos for private use.
|
||||||
|
|
||||||
|
h2 Does this website violate laws and regulations such as once "convert2mp3"?
|
||||||
|
p
|
||||||
|
| No. On "convert2mp3" the videos were converted on the servers of the provider and made available for download.
|
||||||
|
| On our website, you download the videos directly from the sources on the google server.
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
extends _layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
h1 Cannot fetch video
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
extends _layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
div.center
|
||||||
|
h1 Download YouTube videos from source!
|
||||||
|
h2 All Video and Audio Tracks + Thumbnails
|
||||||
|
|
||||||
|
form
|
||||||
|
input(type="url" placeholder="Paste a YouTube video url" name="url")
|
||||||
|
button Fetch
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
div.title-box
|
||||||
|
img(src="logo.png")
|
||||||
|
div
|
||||||
|
h1 Web YouTube Downloader
|
||||||
|
nav
|
||||||
|
a(href="/") Startpage
|
||||||
|
a(href="/about") About
|
||||||
|
a(href="https://github.com/Feuerhamster/web-youtube-downloader/" target="_blank" rel="noopener") GitHub
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
extends _layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
h2 Video
|
||||||
|
iframe(src="https://www.youtube.com/embed/" + id frameborder="0" allow="autoplay; encrypted-media")
|
||||||
|
|
||||||
|
h2 Formats
|
||||||
|
|
||||||
|
table.formats
|
||||||
|
tr
|
||||||
|
th Format
|
||||||
|
th Quality
|
||||||
|
th Bitrate
|
||||||
|
th Container
|
||||||
|
th Codecs
|
||||||
|
th URL
|
||||||
|
each format in formats
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
case format.type
|
||||||
|
when 0
|
||||||
|
span.format.format-0 Video
|
||||||
|
when 1
|
||||||
|
span.format.format-1 Audio
|
||||||
|
when 2
|
||||||
|
span.format.format-2 Adaptive
|
||||||
|
|
||||||
|
td= format.quality
|
||||||
|
td= format.bitrate
|
||||||
|
td(class=format.container)= format.container
|
||||||
|
td= format.codecs
|
||||||
|
td
|
||||||
|
a(href=format.url target="_blank" rel="noreferrer") Download
|
||||||
|
|
||||||
|
h2 Thumbnails
|
||||||
|
table
|
||||||
|
tr
|
||||||
|
th Image
|
||||||
|
th Size
|
||||||
|
th Download
|
||||||
|
each thumbnail in thumbnails
|
||||||
|
tr
|
||||||
|
td
|
||||||
|
img(src=thumbnail.url)
|
||||||
|
|
||||||
|
td= thumbnail.size
|
||||||
|
td
|
||||||
|
a(href=thumbnail.url target="_blank" rel="noreferrer") Download
|
||||||
Loading…
Reference in New Issue