Skip to content

Commit

Permalink
Merge pull request #353 from nodew/auto-start
Browse files Browse the repository at this point in the history
Add option for auto starting download.
  • Loading branch information
alexta69 authored Dec 9, 2023
2 parents 9ef37c2 + 0b77011 commit 4c0dcc8
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 227 deletions.
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
FROM node:18-alpine as builder
FROM node:20-alpine as builder

WORKDIR /metube
COPY ui ./
RUN npm ci && \
node_modules/.bin/ng build --configuration production


FROM python:3.8-alpine
FROM python:3.11-alpine

WORKDIR /app

Expand Down
2 changes: 1 addition & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ python-socketio = "~=5.0"
yt-dlp = "*"

[requires]
python_version = "3.8"
python_version = "3.11"
392 changes: 190 additions & 202 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ Once there, you can use the yt-dlp command freely.

## Building and running locally

Make sure you have node.js and Python 3.8 installed.
Make sure you have node.js and Python 3.11 installed.

```bash
cd metube/ui
Expand Down
12 changes: 10 additions & 2 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,10 @@ async def add(request):
format = post.get('format')
folder = post.get('folder')
custom_name_prefix = post.get('custom_name_prefix')
auto_start = post.get('auto_start')
if custom_name_prefix is None:
custom_name_prefix = ''
status = await dqueue.add(url, quality, format, folder, custom_name_prefix)
status = await dqueue.add(url, quality, format, folder, custom_name_prefix, auto_start)
return web.Response(text=serializer.encode(status))

@routes.post(config.URL_PREFIX + 'delete')
Expand All @@ -132,6 +133,13 @@ async def delete(request):
status = await (dqueue.cancel(ids) if where == 'queue' else dqueue.clear(ids))
return web.Response(text=serializer.encode(status))

@routes.post(config.URL_PREFIX + 'start')
async def start(request):
post = await request.json()
ids = post.get('ids')
status = await dqueue.start_pending(ids)
return web.Response(text=serializer.encode(status))

@routes.get(config.URL_PREFIX + 'history')
async def history(request):
history = { 'done': [], 'queue': []}
Expand Down Expand Up @@ -227,4 +235,4 @@ async def on_prepare(request, response):
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG)
log.info(f"Listening on {config.HOST}:{config.PORT}")
web.run_app(app, host=config.HOST, port=config.PORT, reuse_port=True)
web.run_app(app, host=config.HOST, port=int(config.PORT), reuse_port=True)
36 changes: 26 additions & 10 deletions app/ytdl.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ def __init__(self, id, title, url, quality, format, folder, custom_name_prefix,
self.format = format
self.folder = folder
self.custom_name_prefix = custom_name_prefix
self.status = self.msg = self.percent = self.speed = self.eta = None
self.msg = self.percent = self.speed = self.eta = None
self.status = "pending"
self.timestamp = time.time_ns()
self.error = error

Expand All @@ -59,6 +60,7 @@ def __init__(self, download_dir, temp_dir, output_template, output_template_chap
self.loop = None
self.notifier = None


def _download(self):
try:
def put_status(st):
Expand Down Expand Up @@ -196,13 +198,13 @@ def next(self):
def empty(self):
return not bool(self.dict)


class DownloadQueue:
def __init__(self, config, notifier):
self.config = config
self.notifier = notifier
self.queue = PersistentQueue(self.config.STATE_DIR + '/queue')
self.done = PersistentQueue(self.config.STATE_DIR + '/completed')
self.pending = PersistentQueue(self.config.STATE_DIR + '/pending')
self.done.load()

async def __import_queue(self):
Expand Down Expand Up @@ -246,7 +248,7 @@ def __calc_download_path(self, quality, format, folder):
dldirectory = base_directory
return dldirectory, None

async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, already):
async def __add_entry(self, entry, quality, format, folder, custom_name_prefix, auto_start, already):
if not entry:
return {'status': 'error', 'msg': "Invalid/empty data was given."}

Expand All @@ -270,7 +272,7 @@ async def __add_entry(self, entry, quality, format, folder, custom_name_prefix,
for property in ("id", "title", "uploader", "uploader_id"):
if property in entry:
etr[f"playlist_{property}"] = entry[property]
results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, already))
results.append(await self.__add_entry(etr, quality, format, folder, custom_name_prefix, auto_start, already))
if any(res['status'] == 'error' for res in results):
return {'status': 'error', 'msg': ', '.join(res['msg'] for res in results if res['status'] == 'error' and 'msg' in res)}
return {'status': 'ok'}
Expand All @@ -285,15 +287,18 @@ async def __add_entry(self, entry, quality, format, folder, custom_name_prefix,
for property, value in entry.items():
if property.startswith("playlist"):
output = output.replace(f"%({property})s", str(value))
self.queue.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, self.config.YTDL_OPTIONS, dl))
self.event.set()
if auto_start is True:
self.queue.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, self.config.YTDL_OPTIONS, dl))
self.event.set()
else:
self.pending.put(Download(dldirectory, self.config.TEMP_DIR, output, output_chapter, quality, format, self.config.YTDL_OPTIONS, dl))
await self.notifier.added(dl)
return {'status': 'ok'}
elif etype.startswith('url'):
return await self.add(entry['url'], quality, format, folder, custom_name_prefix, already)
return await self.add(entry['url'], quality, format, folder, custom_name_prefix, auto_start, already)
return {'status': 'error', 'msg': f'Unsupported resource "{etype}"'}

async def add(self, url, quality, format, folder, custom_name_prefix, already=None):
async def add(self, url, quality, format, folder, custom_name_prefix, auto_start=True, already=None):
log.info(f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=}')
already = set() if already is None else already
if url in already:
Expand All @@ -305,7 +310,18 @@ async def add(self, url, quality, format, folder, custom_name_prefix, already=No
entry = await asyncio.get_running_loop().run_in_executor(None, self.__extract_info, url)
except yt_dlp.utils.YoutubeDLError as exc:
return {'status': 'error', 'msg': str(exc)}
return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, already)
return await self.__add_entry(entry, quality, format, folder, custom_name_prefix, auto_start, already)

async def start_pending(self, ids):
for id in ids:
if not self.pending.exists(id):
log.warn(f'requested start for non-existent download {id}')
continue
dl = self.pending.get(id)
self.queue.put(dl)
self.pending.delete(id)
self.event.set()
return {'status': 'ok'}

async def cancel(self, ids):
for id in ids:
Expand Down Expand Up @@ -336,7 +352,7 @@ async def clear(self, ids):
return {'status': 'ok'}

def get(self):
return(list((k, v.info) for k, v in self.queue.items()),
return(list((k, v.info) for k, v in self.queue.items()) + list((k, v.info) for k, v in self.pending.items()),
list((k, v.info) for k, v in self.done.items()))

async def __download(self):
Expand Down
17 changes: 15 additions & 2 deletions ui/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,31 @@
</div>
</div>
<div class="row">
<div class="col-md-5 add-url-component">
<div class="col-md-3 add-url-component">
<div class="input-group">
<span class="input-group-text">Quality</span>
<select class="form-select" name="quality" [(ngModel)]="quality" (change)="qualityChanged()" [disabled]="addInProgress || downloads.loading">
<option *ngFor="let q of qualities" [ngValue]="q.id">{{ q.text }}</option>
</select>
</div>
</div>
<div class="col-md-4 add-url-component">
<div class="col-md-3 add-url-component">
<div class="input-group">
<span class="input-group-text">Format</span>
<select class="form-select" name="format" [(ngModel)]="format" (change)="formatChanged()" [disabled]="addInProgress || downloads.loading">
<option *ngFor="let f of formats" [ngValue]="f.id">{{ f.text }}</option>
</select>
</div>
</div>
<div class="col-md-3 add-url-component">
<div class="input-group">
<span class="input-group-text">Auto Start</span>
<select class="form-select" name="autoStart" [(ngModel)]="autoStart" (change)="autoStartChanged()" [disabled]="addInProgress || downloads.loading">
<option [ngValue]="true">Yes</option>
<option [ngValue]="false">No</option>
</select>
</div>
</div>
<div class="col-md-3 add-url-component">
<div [attr.class]="showAdvanced() ? 'btn-group add-url-group' : 'add-url-group'" ngbDropdown #advancedDropdown="ngbDropdown" display="dynamic" placement="bottom-end">
<button class="btn btn-primary add-url" type="submit" (click)="addDownload()" [disabled]="addInProgress || downloads.loading">
Expand Down Expand Up @@ -111,6 +120,7 @@
<th scope="col" style="width: 7rem;">ETA</th>
<th scope="col" style="width: 2rem;"></th>
<th scope="col" style="width: 2rem;"></th>
<th scope="col" style="width: 2rem;"></th>
</tr>
</thead>
<tbody>
Expand All @@ -122,6 +132,9 @@
<td><ngb-progressbar height="1.5rem" [showValue]="download.value.status != 'preparing'" [striped]="download.value.status == 'preparing'" [animated]="download.value.status == 'preparing'" type="success" [value]="download.value.status == 'preparing' ? 100 : download.value.percent | number:'1.0-0'"></ngb-progressbar></td>
<td>{{ download.value.speed | speed }}</td>
<td>{{ download.value.eta | eta }}</td>
<td>
<button *ngIf="download.value.status === 'pending'" type="button" class="btn btn-link" (click)="downloadItemByKey(download.key)"><fa-icon [icon]="faDownload"></fa-icon></button>
</td>
<td><button type="button" class="btn btn-link" (click)="delDownload('queue', download.key)"><fa-icon [icon]="faTrashAlt"></fa-icon></button></td>
<td><a href="{{download.value.url}}" target="_blank"><fa-icon [icon]="faExternalLinkAlt"></fa-icon></a></td>
</tr>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/app/app.component.sass
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
outline: 0px

.add-url-box
max-width: 720px
max-width: 960px
margin: 4rem auto

.add-url-component
Expand Down
20 changes: 16 additions & 4 deletions ui/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class AppComponent implements AfterViewInit {
format: string;
folder: string;
customNamePrefix: string;
autoStart: boolean;
addInProgress = false;
themes: Theme[] = Themes;
activeTheme: Theme;
Expand Down Expand Up @@ -51,6 +52,8 @@ export class AppComponent implements AfterViewInit {
// Needs to be set or qualities won't automatically be set
this.setQualities()
this.quality = cookieService.get('metube_quality') || 'best';
this.autoStart = cookieService.get('metube_auto_start') === 'true';

this.activeTheme = this.getPreferredTheme(cookieService);
}

Expand Down Expand Up @@ -154,6 +157,10 @@ export class AppComponent implements AfterViewInit {
this.downloads.customDirsChanged.next(this.downloads.customDirs);
}

autoStartChanged() {
this.cookieService.set('metube_auto_start', this.autoStart ? 'true' : 'false', { expires: 3650 });
}

queueSelectionChanged(checked: number) {
this.queueDelSelected.nativeElement.disabled = checked == 0;
}
Expand All @@ -169,16 +176,17 @@ export class AppComponent implements AfterViewInit {
this.quality = exists ? this.quality : 'best'
}

addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string) {
addDownload(url?: string, quality?: string, format?: string, folder?: string, customNamePrefix?: string, autoStart?: boolean) {
url = url ?? this.addUrl
quality = quality ?? this.quality
format = format ?? this.format
folder = folder ?? this.folder
customNamePrefix = customNamePrefix ?? this.customNamePrefix
autoStart = autoStart ?? this.autoStart

console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix);
console.debug('Downloading: url='+url+' quality='+quality+' format='+format+' folder='+folder+' customNamePrefix='+customNamePrefix+' autoStart='+autoStart);
this.addInProgress = true;
this.downloads.add(url, quality, format, folder, customNamePrefix).subscribe((status: Status) => {
this.downloads.add(url, quality, format, folder, customNamePrefix, autoStart).subscribe((status: Status) => {
if (status.status === 'error') {
alert(`Error adding URL: ${status.msg}`);
} else {
Expand All @@ -188,8 +196,12 @@ export class AppComponent implements AfterViewInit {
});
}

downloadItemByKey(id: string) {
this.downloads.startById([id]).subscribe();
}

retryDownload(key: string, download: Download) {
this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix);
this.addDownload(download.url, download.quality, download.format, download.folder, download.custom_name_prefix, true);
this.downloads.delById('done', [key]).subscribe();
}

Expand Down
8 changes: 6 additions & 2 deletions ui/src/app/downloads.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,16 @@ export class DownloadsService {
return of({status: 'error', msg: msg})
}

public add(url: string, quality: string, format: string, folder: string, customNamePrefix: string) {
return this.http.post<Status>('add', {url: url, quality: quality, format: format, folder: folder, custom_name_prefix: customNamePrefix}).pipe(
public add(url: string, quality: string, format: string, folder: string, customNamePrefix: string, autoStart: boolean) {
return this.http.post<Status>('add', {url: url, quality: quality, format: format, folder: folder, custom_name_prefix: customNamePrefix, auto_start: autoStart}).pipe(
catchError(this.handleHTTPError)
);
}

public startById(ids: string[]) {
return this.http.post('start', {ids: ids});
}

public delById(where: string, ids: string[]) {
ids.forEach(id => this[where].get(id).deleting = true);
return this.http.post('delete', {where: where, ids: ids});
Expand Down

0 comments on commit 4c0dcc8

Please sign in to comment.