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