Merge pull request #13 from marcopeocchi/feat_download_path_selection
Feat download path selection
This commit is contained in:
11
Dockerfile
11
Dockerfile
@@ -9,13 +9,10 @@ RUN apk add curl wget psmisc python3 ffmpeg
|
||||
COPY . .
|
||||
RUN chmod +x ./fetch-yt-dlp.sh
|
||||
# install node dependencies
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
RUN npm run build-server
|
||||
RUN npm run fetch
|
||||
# cleanup
|
||||
RUN npm remove parcel
|
||||
RUN rm -rf .parcel-cache
|
||||
RUN yarn
|
||||
RUN yarn build
|
||||
RUN yarn build-server
|
||||
RUN yarn run fetch
|
||||
# expose and run
|
||||
EXPOSE 3022
|
||||
CMD [ "node" , "./dist/main.js" ]
|
||||
|
||||
@@ -5,10 +5,13 @@ import {
|
||||
ButtonGroup,
|
||||
CircularProgress,
|
||||
Container,
|
||||
FormControl,
|
||||
Grid,
|
||||
IconButton,
|
||||
InputAdornment,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Snackbar,
|
||||
styled,
|
||||
TextField,
|
||||
@@ -44,6 +47,9 @@ export default function Home({ socket }: Props) {
|
||||
const [pickedAudioFormat, setPickedAudioFormat] = useState('');
|
||||
const [pickedBestFormat, setPickedBestFormat] = useState('');
|
||||
|
||||
const [downloadPath, setDownloadPath] = useState<number>(0);
|
||||
const [availableDownloadPaths, setAvailableDownloadPaths] = useState<string[]>([]);
|
||||
|
||||
const [url, setUrl] = useState('');
|
||||
const [workingUrl, setWorkingUrl] = useState('');
|
||||
const [showBackdrop, setShowBackdrop] = useState(false);
|
||||
@@ -106,6 +112,14 @@ export default function Home({ socket }: Props) {
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${window.location.protocol}//${settings.serverAddr}:${settings.serverPort}/tree`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setAvailableDownloadPaths(data.flat)
|
||||
})
|
||||
}, [])
|
||||
|
||||
/* -------------------- component functions -------------------- */
|
||||
|
||||
/**
|
||||
@@ -119,6 +133,7 @@ export default function Home({ socket }: Props) {
|
||||
|
||||
socket.emit('send-url', {
|
||||
url: immediate || url || workingUrl,
|
||||
path: availableDownloadPaths[downloadPath],
|
||||
params: settings.cliArgs.toString() + toFormatArgs(codes),
|
||||
})
|
||||
setUrl('')
|
||||
@@ -211,25 +226,44 @@ export default function Home({ socket }: Props) {
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
id="urlInput"
|
||||
label={settings.i18n.t('urlInput')}
|
||||
variant="outlined"
|
||||
onChange={handleUrlChange}
|
||||
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<label htmlFor="icon-button-file">
|
||||
<Input id="icon-button-file" type="file" accept=".txt" onChange={parseUrlListFile} />
|
||||
<IconButton color="primary" aria-label="upload file" component="span">
|
||||
<FileUpload />
|
||||
</IconButton>
|
||||
</label>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item xs={10}>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="urlInput"
|
||||
label={settings.i18n.t('urlInput')}
|
||||
variant="outlined"
|
||||
onChange={handleUrlChange}
|
||||
disabled={!status.connected || (settings.formatSelection && downloadFormats != null)}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<label htmlFor="icon-button-file">
|
||||
<Input id="icon-button-file" type="file" accept=".txt" onChange={parseUrlListFile} />
|
||||
<IconButton color="primary" aria-label="upload file" component="span">
|
||||
<FileUpload />
|
||||
</IconButton>
|
||||
</label>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={2}>
|
||||
<FormControl fullWidth>
|
||||
<Select
|
||||
defaultValue={0}
|
||||
value={availableDownloadPaths[downloadPath]}
|
||||
onChange={(e) => setDownloadPath(e.target.value)}
|
||||
>
|
||||
|
||||
{availableDownloadPaths.map((val: string, idx: number) => (
|
||||
<MenuItem key={idx} value={idx}>{val}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container spacing={1} pt={2}>
|
||||
<Grid item>
|
||||
<Button
|
||||
@@ -365,7 +399,7 @@ export default function Home({ socket }: Props) {
|
||||
<Grid container spacing={{ xs: 2, md: 2 }} columns={{ xs: 4, sm: 8, md: 12 }} pt={2}>
|
||||
{ /*Super big brain flatMap moment*/
|
||||
Array
|
||||
.from(messageMap)
|
||||
.from<any>(messageMap)
|
||||
.filter(flattened => [...flattened][0])
|
||||
.filter(flattened => [...flattened][1].toString() !== serverStates.PROG_DONE)
|
||||
.flatMap(message => (
|
||||
|
||||
@@ -41,6 +41,12 @@ class Process {
|
||||
public async start(callback?: Function): Promise<this> {
|
||||
const sanitizedParams = this.params.filter((param: string) => availableParams.includes(param));
|
||||
|
||||
if (this.settings?.download_path) {
|
||||
if (this.settings.download_path.charAt(this.settings.download_path.length - 1) !== '/') {
|
||||
this.settings.download_path = `${this.settings.download_path}/`
|
||||
}
|
||||
}
|
||||
|
||||
const ytldp = spawn(this.exePath,
|
||||
['-o', `${this.settings?.download_path || 'downloads/'}%(title)s.%(ext)s`]
|
||||
.concat(sanitizedParams)
|
||||
|
||||
@@ -61,7 +61,12 @@ export async function download(socket: Socket, payload: IPayload) {
|
||||
payload.params.split(' ') :
|
||||
payload.params;
|
||||
|
||||
let p = new Process(url, params, settings);
|
||||
const scopedSettings: ISettings = {
|
||||
...settings,
|
||||
download_path: payload.path
|
||||
}
|
||||
|
||||
let p = new Process(url, params, scopedSettings);
|
||||
|
||||
p.start().then(downloader => {
|
||||
mem_db.add(downloader)
|
||||
@@ -111,7 +116,10 @@ function streamProcess(process: Process, socket: Socket) {
|
||||
map(stdout => formatter(String(stdout), process.getPid()))
|
||||
)
|
||||
.subscribe({
|
||||
next: (stdout) => socket.emit('progress', stdout),
|
||||
next: (stdout) => {
|
||||
socket.emit('progress', stdout)
|
||||
log.info(`proc-${stdout.pid}`, `${stdout.progress}\t${stdout.dlSpeed}`)
|
||||
},
|
||||
complete: () => {
|
||||
process.kill().then(() => {
|
||||
emitAbort();
|
||||
|
||||
1
server/src/interfaces/IPayload.d.ts
vendored
1
server/src/interfaces/IPayload.d.ts
vendored
@@ -5,6 +5,7 @@
|
||||
export interface IPayload {
|
||||
url: string
|
||||
params: Array<string> | string,
|
||||
path: string,
|
||||
title?: string,
|
||||
thumbnail?: string,
|
||||
size?: string,
|
||||
|
||||
@@ -2,7 +2,13 @@ import { splash } from './utils/logger';
|
||||
import { join } from 'path';
|
||||
import { Server } from 'socket.io';
|
||||
import { ytdlpUpdater } from './utils/updater';
|
||||
import { download, abortDownload, retrieveDownload, abortAllDownloads, getFormatsAndMetadata } from './core/downloader';
|
||||
import {
|
||||
download,
|
||||
abortDownload,
|
||||
retrieveDownload,
|
||||
abortAllDownloads,
|
||||
getFormatsAndMetadata
|
||||
} from './core/downloader';
|
||||
import { getFreeDiskSpace } from './utils/procUtils';
|
||||
import { listDownloaded } from './core/downloadArchive';
|
||||
import { createServer } from 'http';
|
||||
@@ -13,6 +19,7 @@ import * as serve from 'koa-static';
|
||||
import * as cors from '@koa/cors';
|
||||
import Logger from './utils/BetterLogger';
|
||||
import { ISettings } from './interfaces/ISettings';
|
||||
import { directoryTree } from './utils/directoryUtils';
|
||||
|
||||
const app = new Koa();
|
||||
const server = createServer(app.callback());
|
||||
@@ -56,6 +63,10 @@ router.get('/archive', (ctx, next) => {
|
||||
router.get('/stream/:filepath', (ctx, next) => {
|
||||
streamer(ctx, next)
|
||||
})
|
||||
router.get('/tree', (ctx, next) => {
|
||||
ctx.body = directoryTree()
|
||||
next()
|
||||
})
|
||||
|
||||
// WebSocket listeners
|
||||
io.on('connection', socket => {
|
||||
|
||||
62
server/src/utils/directoryUtils.ts
Normal file
62
server/src/utils/directoryUtils.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { readdirSync, statSync } from "fs";
|
||||
import { ISettings } from "../interfaces/ISettings";
|
||||
|
||||
let settings: ISettings;
|
||||
|
||||
class Node {
|
||||
public path: string
|
||||
public children: Node[]
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path
|
||||
this.children = []
|
||||
}
|
||||
}
|
||||
|
||||
function buildTreeDFS(rootPath: string, directoryOnly: boolean) {
|
||||
const root = new Node(rootPath)
|
||||
const stack: Node[] = []
|
||||
const flattened: string[] = []
|
||||
|
||||
stack.push(root)
|
||||
flattened.push(rootPath)
|
||||
|
||||
while (stack.length) {
|
||||
const current = stack.pop()
|
||||
if (current) {
|
||||
const children = readdirSync(current.path)
|
||||
for (const it of children) {
|
||||
const childPath = `${current.path}/${it}`
|
||||
const childNode = new Node(childPath)
|
||||
|
||||
if (directoryOnly) {
|
||||
if (statSync(childPath).isDirectory()) {
|
||||
current.children.push(childNode)
|
||||
stack.push(childNode)
|
||||
flattened.push(childNode.path)
|
||||
}
|
||||
} else {
|
||||
current.children.push(childNode)
|
||||
if (statSync(childPath).isDirectory()) {
|
||||
stack.push(childNode)
|
||||
flattened.push(childNode.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
tree: root,
|
||||
flat: flattened
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
settings = require('../../settings.json');
|
||||
} catch (e) { }
|
||||
|
||||
export function directoryTree() {
|
||||
const tree = buildTreeDFS(settings.download_path || 'downloads', true)
|
||||
return tree
|
||||
}
|
||||
@@ -17,8 +17,8 @@ export const splash = () => {
|
||||
const reset = "\u001b[0m"
|
||||
console.log(`${fg} __ ____ __ __ ______`)
|
||||
console.log(" __ __/ /________/ / /__ _ _____ / / / / / / _/")
|
||||
console.log(" / // / __/___/ _ / / _ \ | |/|/ / -_) _ \/ /_/ // / ")
|
||||
console.log(" \_, /\__/ \_,_/_/ .__/ |__,__/\__/_.__/\____/___/ ")
|
||||
console.log(" / // / __/___/ _ / / _ \\ | |/|/ / -_) _ \\/ /_/ // / ")
|
||||
console.log(" \\_, /\\__/ \\_,_/_/ .__/ |__,__/\\__/_.__/\\____/___/ ")
|
||||
console.log(`/___/ /_/ \n${reset}`)
|
||||
console.log(" yt-dlp-webUI - A web-ui for yt-dlp, simply enough")
|
||||
console.log("---------------------------------------------------\n")
|
||||
|
||||
Reference in New Issue
Block a user