mirror of
https://github.com/tedkulp/vidgrab
synced 2026-03-05 13:20:27 -05:00
Switch from handlebars to next.js
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
dist
|
||||
node_modules
|
||||
downloads
|
||||
.next
|
||||
|
||||
@@ -3,6 +3,9 @@ module.exports = {
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'simple-import-sort'],
|
||||
extends: [
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -34,3 +34,4 @@ lerna-debug.log*
|
||||
!.vscode/extensions.json
|
||||
|
||||
/downloads
|
||||
.next
|
||||
|
||||
@@ -3,6 +3,8 @@ services:
|
||||
redis:
|
||||
image: redis
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- 6379:6379
|
||||
vidgrab:
|
||||
image: tedkulp/vidgrab
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src"
|
||||
}
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"tsConfigPath": "tsconfig.server.json"
|
||||
}
|
||||
}
|
||||
3
next-env.d.ts
vendored
Normal file
3
next-env.d.ts
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
3890
package-lock.json
generated
3890
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,8 +29,11 @@
|
||||
"bull": "^3.26.0",
|
||||
"class-transformer": "^0.4.0",
|
||||
"class-validator": "^0.13.1",
|
||||
"express-handlebars": "^5.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"nest-next": "^9.4.0",
|
||||
"next": "^11.0.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs": "^6.6.6",
|
||||
@@ -47,6 +50,8 @@
|
||||
"@types/jest": "^26.0.22",
|
||||
"@types/lodash": "^4.14.171",
|
||||
"@types/node": "^14.14.36",
|
||||
"@types/react": "^17.0.14",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/split2": "^3.2.1",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^4.19.0",
|
||||
@@ -81,4 +86,4 @@
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
pages/_app.jsx
Normal file
6
pages/_app.jsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import '../styles/styles.css';
|
||||
|
||||
// This default export is required in a new `pages/_app.js` file.
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
48
pages/_document.jsx
Normal file
48
pages/_document.jsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import Document, { Head, Html, Main, NextScript } from 'next/document';
|
||||
|
||||
class MyDocument extends Document {
|
||||
static async getInitialProps(ctx) {
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return { ...initialProps };
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css"
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<section className="section has-background-light">
|
||||
<div className="container">
|
||||
<nav className="level">
|
||||
<h1 className="title is-1 level-item has-text-centered">
|
||||
Vidgrab
|
||||
</h1>
|
||||
</nav>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
|
||||
<footer id="footer" className="footer">
|
||||
<div className="content has-text-centered">
|
||||
<p>
|
||||
Code and "Design" by <a href="#">Ted Kulp</a>. Based on
|
||||
youtube-dl.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
||||
40
pages/views/Extractors.tsx
Normal file
40
pages/views/Extractors.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { NextPage, NextPageContext } from 'next';
|
||||
|
||||
// The component's props type
|
||||
type PageProps = {
|
||||
extractors?: string[];
|
||||
};
|
||||
|
||||
// extending the default next context type
|
||||
type PageContext = NextPageContext & {
|
||||
query: PageProps;
|
||||
};
|
||||
|
||||
// react component
|
||||
const Page: NextPage<PageProps> = ({ extractors }) => {
|
||||
return (
|
||||
<>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<div className="content">
|
||||
<p>
|
||||
Here is the list of currently supported services. This is
|
||||
generated dynamically because youtube-dl adds new services all the
|
||||
time.
|
||||
</p>
|
||||
<ul>{extractors && extractors.map((e) => <li key={e}>{e}</li>)}</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// assigning the initial props to the component's props
|
||||
Page.getInitialProps = async ({ query }: PageContext) => {
|
||||
return {
|
||||
extractors: query?.extractors,
|
||||
};
|
||||
};
|
||||
|
||||
export default Page;
|
||||
68
pages/views/Getinfo.tsx
Normal file
68
pages/views/Getinfo.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { NextPage, NextPageContext } from 'next';
|
||||
|
||||
// The component's props type
|
||||
type PageProps = {
|
||||
title: string;
|
||||
extractor: string;
|
||||
description: string;
|
||||
videoUrl: string;
|
||||
thumbnails: string;
|
||||
upload_date: string;
|
||||
duration: number;
|
||||
formats: any[];
|
||||
};
|
||||
|
||||
// extending the default next context type
|
||||
type PageContext = NextPageContext & {
|
||||
query: PageProps;
|
||||
};
|
||||
|
||||
// react component
|
||||
const Page: NextPage<PageProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<form method="POST" action="/queue">
|
||||
<div className="field is-grouped">
|
||||
<div className="control is-expanded">
|
||||
<div className="select is-fullwidth">
|
||||
<select name="format">
|
||||
{props.formats &&
|
||||
props.formats.map((f) => (
|
||||
<option key={f.format_id} value={f.format_id}>
|
||||
{f.format} ({f.ext})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="control">
|
||||
<button
|
||||
className="button is-primary"
|
||||
id="form-submit"
|
||||
type="submit"
|
||||
>
|
||||
Queue Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="url" value={props.videoUrl} />
|
||||
<input type="hidden" name="title" value={props.title} />
|
||||
<input type="hidden" name="extractor" value={props.extractor} />
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// assigning the initial props to the component's props
|
||||
Page.getInitialProps = async ({ query }: PageContext) => {
|
||||
return {
|
||||
...query,
|
||||
};
|
||||
};
|
||||
|
||||
export default Page;
|
||||
115
pages/views/Index.tsx
Normal file
115
pages/views/Index.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import { NextPage, NextPageContext } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
// The component's props type
|
||||
type PageProps = {
|
||||
bookmarklet?: string;
|
||||
jobs?: any[];
|
||||
};
|
||||
|
||||
// extending the default next context type
|
||||
type PageContext = NextPageContext & {
|
||||
query: PageProps;
|
||||
};
|
||||
|
||||
// react component
|
||||
const Page: NextPage<PageProps> = ({ bookmarklet, jobs }) => {
|
||||
return (
|
||||
<>
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<form method="POST" action="/getinfo">
|
||||
<div className="field is-grouped">
|
||||
<div className="control is-expanded">
|
||||
<input
|
||||
className="input is-medium"
|
||||
name="url"
|
||||
placeholder="URL to Download"
|
||||
/>
|
||||
<p className="help">
|
||||
<Link href="/extractors">
|
||||
* List of currently available services
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="control">
|
||||
<button
|
||||
className="button is-primary is-medium"
|
||||
id="form-submit"
|
||||
type="submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="container content has-text-centered">
|
||||
<p>Drag this to your bookmark bar!</p>
|
||||
<a
|
||||
className="bookmarklet"
|
||||
ref={(node) =>
|
||||
node && bookmarklet && node.setAttribute('href', bookmarklet)
|
||||
}
|
||||
>
|
||||
Vidgrab It!
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<h2 className="subtitle is-4 has-text-centered">Recent Downloads</h2>
|
||||
|
||||
<table className="table is-striped is-hoverable is-fullwidth is-narrow">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>
|
||||
<abbr title="Service">Svc</abbr>
|
||||
</th>
|
||||
<th>Title</th>
|
||||
<th>
|
||||
<abbr title="Percent Downloaded">%</abbr>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{jobs &&
|
||||
jobs.map((j) => (
|
||||
<tr key={j.id}>
|
||||
<td>{j.id}</td>
|
||||
<td>{j.state}</td>
|
||||
<td>{j.data.extractor}</td>
|
||||
<td>
|
||||
<a
|
||||
href={j.data.url}
|
||||
title={j.data.title}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{j.data.title}
|
||||
</a>
|
||||
</td>
|
||||
<td>{j.progress}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// assigning the initial props to the component's props
|
||||
Page.getInitialProps = async ({ query }: PageContext) => {
|
||||
return {
|
||||
bookmarklet: query?.bookmarklet?.toString(),
|
||||
jobs: query?.jobs,
|
||||
};
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||
import { RenderModule } from 'nest-next';
|
||||
import Next from 'next';
|
||||
|
||||
import configuration from './config/configuration';
|
||||
import { WebModule } from './web/web.module';
|
||||
@@ -13,6 +15,9 @@ import { YtdlModule } from './ytdl/ytdl.module';
|
||||
isGlobal: true,
|
||||
load: [configuration],
|
||||
}),
|
||||
RenderModule.forRootAsync(
|
||||
Next({ dev: process.env.NODE_ENV !== 'production' }),
|
||||
),
|
||||
BullModule.forRootAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: (configService: ConfigService) => ({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export default () => ({
|
||||
redisHost: process.env.REDIS_HOST || 'localhost',
|
||||
redisPort: parseInt(process.env.REDIS_PORT, 10) || 6379,
|
||||
redisPort: process.env.REDIS_PORT
|
||||
? parseInt(process.env.REDIS_PORT, 10)
|
||||
: 6379,
|
||||
fileDir: process.env.FILE_DIR || '/tmp',
|
||||
});
|
||||
|
||||
20
src/main.ts
20
src/main.ts
@@ -1,29 +1,11 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import * as exphbs from 'express-handlebars';
|
||||
import { capitalize, truncate } from 'lodash';
|
||||
import { join } from 'path';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||||
|
||||
const hbs = exphbs.create({
|
||||
defaultLayout: 'main',
|
||||
layoutsDir: join(__dirname, '..', 'views', 'layouts'),
|
||||
helpers: {
|
||||
capitalize,
|
||||
truncate: (str: string, options: any) => truncate(str, options.hash),
|
||||
},
|
||||
extname: '.hbs',
|
||||
});
|
||||
|
||||
app.useStaticAssets(join(__dirname, '..', 'public'));
|
||||
app.setBaseViewsDir(join(__dirname, '..', 'views'));
|
||||
app.engine('.hbs', hbs.engine);
|
||||
app.setViewEngine('.hbs');
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
export class UploadDto {
|
||||
url: string;
|
||||
url = '';
|
||||
}
|
||||
|
||||
export class QueueDto {
|
||||
format? = 'best';
|
||||
url: string;
|
||||
url = '';
|
||||
title?: string;
|
||||
extractor?: string;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
Logger,
|
||||
Post,
|
||||
Redirect,
|
||||
@@ -27,7 +28,7 @@ export class WebController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Render('index')
|
||||
@Render('Index')
|
||||
async root(@Req() request: any) {
|
||||
const fullUrl =
|
||||
request.protocol + '://' + request.get('host') + request.originalUrl;
|
||||
@@ -51,7 +52,7 @@ export class WebController {
|
||||
const progress = j.progress();
|
||||
|
||||
return {
|
||||
...j,
|
||||
...pick(j, ['id', 'name', 'data']),
|
||||
progress: progress ? `${progress}%` : 'n/a',
|
||||
state: state,
|
||||
};
|
||||
@@ -60,14 +61,20 @@ export class WebController {
|
||||
|
||||
return {
|
||||
bookmarklet,
|
||||
jobs: jobList
|
||||
.sort((a, b) => parseInt(b.id.toString()) - parseInt(a.id.toString()))
|
||||
.slice(0, 10),
|
||||
jobs: JSON.parse(
|
||||
JSON.stringify(
|
||||
jobList
|
||||
.sort(
|
||||
(a, b) => parseInt(b.id.toString()) - parseInt(a.id.toString()),
|
||||
)
|
||||
.slice(0, 10),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('/extractors')
|
||||
@Render('extractors')
|
||||
@Render('Extractors')
|
||||
async listExtractors() {
|
||||
const extractors = await this.ytdlService.listExtractors();
|
||||
|
||||
@@ -77,7 +84,8 @@ export class WebController {
|
||||
}
|
||||
|
||||
@Post('/getinfo')
|
||||
@Render('getinfo')
|
||||
@Render('Getinfo')
|
||||
@HttpCode(200)
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async getInfo(@Body() body: UploadDto) {
|
||||
const videoInfo = await this.ytdlService.getVideoInfo(body.url);
|
||||
@@ -86,7 +94,7 @@ export class WebController {
|
||||
title: videoInfo.title,
|
||||
extractor: videoInfo.extractor,
|
||||
description: videoInfo.description,
|
||||
url: videoInfo.webpage_url,
|
||||
videoUrl: videoInfo.webpage_url,
|
||||
thumbnails: videoInfo.thumbnails,
|
||||
upload_date: videoInfo.upload_date,
|
||||
duration: videoInfo.duration,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Job } from 'bull';
|
||||
import { throttle } from 'lodash';
|
||||
import * as split from 'split2';
|
||||
import split from 'split2';
|
||||
import { QueueDto } from 'src/types';
|
||||
import { raw } from 'youtube-dl-exec';
|
||||
|
||||
@@ -38,16 +38,16 @@ export class YtdlProcessor {
|
||||
newline: true,
|
||||
});
|
||||
|
||||
execaProcess.stdout.pipe(split()).on('data', (line) => {
|
||||
execaProcess.stdout?.pipe(split()).on('data', (line: string) => {
|
||||
this.logger.verbose(`youtube-dl stdout: ${line}`);
|
||||
|
||||
const percentDone = line.match(/\[download\]\s+([0-9.]+)% of/);
|
||||
if (percentDone) {
|
||||
this.setProgress(job, percentDone[1]);
|
||||
this.setProgress(job, parseFloat(percentDone[1]));
|
||||
}
|
||||
});
|
||||
|
||||
execaProcess.stderr.pipe(split()).on('data', (line) => {
|
||||
execaProcess.stderr?.pipe(split()).on('data', (line: any) => {
|
||||
this.logger.error(`youtube-dl stderr: ${line}`);
|
||||
});
|
||||
|
||||
|
||||
19
styles/styles.css
Normal file
19
styles/styles.css
Normal file
@@ -0,0 +1,19 @@
|
||||
body {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding-top: 2em;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.bookmarklet {
|
||||
border: 1px dashed black;
|
||||
padding: 10px 20px 10px 20px;
|
||||
}
|
||||
@@ -1,15 +1,44 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"target": "esnext",
|
||||
"module": "esnext",
|
||||
"jsx": "preserve",
|
||||
"strict": true,
|
||||
"pretty": true,
|
||||
"noImplicitAny": true,
|
||||
"alwaysStrict": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"outDir": ".next",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2017",
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"baseUrl": "./",
|
||||
"incremental": true
|
||||
}
|
||||
}
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"typeRoots": [
|
||||
"node_modules/@types",
|
||||
"./typings"
|
||||
],
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true
|
||||
},
|
||||
"include": [
|
||||
"./pages/**/*",
|
||||
"./src/**/*",
|
||||
"./ui/**/*",
|
||||
"types.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
22
tsconfig.server.json
Normal file
22
tsconfig.server.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"declaration": true,
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"isolatedModules": false,
|
||||
"noEmit": false,
|
||||
"allowJs": false
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"types.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
<p>Here is the list of currently supported services. This is generated dynamically because youtube-dl adds new
|
||||
services all the time.</p>
|
||||
<ul>
|
||||
{{#each extractors}}
|
||||
<li>{{this}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,33 +0,0 @@
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<form method="POST" action="/queue">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<div class="select is-fullwidth">
|
||||
<select name="format">
|
||||
{{#each formats}}
|
||||
<option value={{format_id}}>{{format}} ({{ext}})</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control">
|
||||
<button class="button is-primary" id="form-submit" type="submit">Queue Download</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="url" value="{{url}}" />
|
||||
<input type="hidden" name="title" value="{{title}}" />
|
||||
<input type="hidden" name="extractor" value="{{extractor}}" />
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function changeButton() {
|
||||
var ele = document.getElementById('form-submit');
|
||||
if (ele) {
|
||||
ele.classList.toggle('is-loading');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,60 +0,0 @@
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<form method="POST" action="/getinfo" onsubmit="changeButton()">
|
||||
<div class="field is-grouped">
|
||||
<div class="control is-expanded">
|
||||
<input class="input is-medium" name="url" placeholder="URL to Download" />
|
||||
<p class="help"><a href="/extractors">* List of currently available services</a></p>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-primary is-medium" id="form-submit" type="submit">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="container content has-text-centered">
|
||||
<p>Drag this to your bookmark bar!</p>
|
||||
<a class="bookmarklet" href="{{bookmarklet}}">Vidgrab It!</a>
|
||||
</div>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h2 class="subtitle is-4 has-text-centered">Recent Downloads</h2>
|
||||
|
||||
<table class="table is-striped is-hoverable is-fullwidth is-narrow">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th><abbr title="Service">Svc</abbr></th>
|
||||
<th>Title</th>
|
||||
<th><abbr title="Percent Downloaded">%</abbr></th>
|
||||
</tr>
|
||||
</thead>
|
||||
{{#each jobs}}
|
||||
<tr>
|
||||
<td>{{id}}</td>
|
||||
<td>{{capitalize state}}</td>
|
||||
<td>{{capitalize data.extractor}}</td>
|
||||
<td>
|
||||
<a href="{{data.url}}" title="{{data.title}}" target="_blank" rel="noopener noreferrer">
|
||||
{{data.title}}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{progress}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function changeButton() {
|
||||
var ele = document.getElementById('form-submit');
|
||||
if (ele) {
|
||||
ele.classList.toggle('is-loading');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,55 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Vidgrab</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.min.css">
|
||||
<style type="text/css" media="screen">
|
||||
body {
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding-top: 2em;
|
||||
padding-bottom: 2em;
|
||||
}
|
||||
|
||||
.bookmarklet {
|
||||
border: 1px dashed black;
|
||||
padding: 10px 20px 10px 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<section class="section has-background-light">
|
||||
<div class="container">
|
||||
<nav class="level">
|
||||
<h1 class="title is-1 level-item has-text-centered">Vidgrab</h1>
|
||||
</nav>
|
||||
<div>
|
||||
</section>
|
||||
|
||||
{{{body}}}
|
||||
|
||||
</div>
|
||||
|
||||
<footer id="footer" class="footer">
|
||||
<div class="content has-text-centered">
|
||||
<p>
|
||||
Code and "Design" by <a href="#">Ted Kulp</a>. Based on youtube-dl.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user