Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-Threaded downloading and processing #1918

Open
4 tasks done
Saklad5 opened this issue Dec 7, 2021 · 31 comments
Open
4 tasks done

Multi-Threaded downloading and processing #1918

Saklad5 opened this issue Dec 7, 2021 · 31 comments
Labels

Comments

@Saklad5
Copy link

Saklad5 commented Dec 7, 2021

Checklist

Description

Problem

When instructed to download multiple URLs, yt-dlp works completely serially: it retrieves, extracts, and downloads the desired content (presumably saturating the network connection), then performs postprocessing (potentially saturating the CPU, among other things).

These tasks have broadly independent constraints: there is minimal CPU demand for downloading a video, and no network traffic while transcoding it. This leaves a lot of room for improvement, which is particularly noticeable with common tasks like downloading playlists.

Fix

I propose a form of pipelining: when network-constrained tasks finish (that is, when downloading is complete), yt-dlp should move onto the next item’s network-constrained tasks. This means that the first item can be postprocessed while the second item is downloading.

If the second item finishes downloading before the first item is done with postprocessing, yt-dlp can add it to the postprocessing queue and move onto downloading the third item.

The result is that a downloading operation and a postprocessing operation can occur at the same time, substantially improving speed without incurring thrashing.

Implementation

yt-dlp already draws a concrete distinction between downloading and postprocessing, which is used for some of the options. That should make it relatively seamless to decouple them for this purpose without breaking any custom plugins.

Moreover, this scheme would never run more than one instance of a task at the same time (no task runs both before and after downloading is finished), so shared state is unlikely to be a concern.

The only thing that may be complicated by this proposal, then, is the UI. At worst, this could be dealt with by simply not displaying progress when run in this manner.

@Saklad5 Saklad5 added enhancement New feature or request triage Untriaged issue labels Dec 7, 2021
@pukkandan

This comment has been minimized.

@pukkandan pukkandan removed the triage Untriaged issue label Dec 7, 2021
@pukkandan pukkandan changed the title Pipeline downloading and processing for multiple URLs Multi-Threaded downloading and processing Dec 7, 2021
@Jules-A
Copy link
Contributor

Jules-A commented Dec 7, 2021

To get around this issue I've been using this command: yt-dlp -v "https://www.funimation.com/shows/spice-and-wolf/" --config-location funimation.conf --exec "start /B yt-dlp --config-location funimation.conf -q --fixup force --embed-subs --load-info-json %%(__infojson_filename)q" --write-subs --download-archive archive.txt --ffmpeg-location "D:\dummy" --write-info-json -P "D:\Temp"

conf: -f '(bv*+ba/b)[format_note=Uncut] / (bv*+ba/b)' -o '%(extractor)s\%(title)s%(myindex)s.%(ext)s' -P 'D:\Videos' -P 'temp:D:\Temp' --parse-metadata 'original_url:#%(playlist_index)s' --parse-metadata ' - %(playlist_index)d:^(?P<myindex> - \d+)$' --parse-metadata '%(series)s - S%(season_number)sE%(episode_number)s - %(episode)s:^(?P<title>.+ - S\d+E\d+ - \S+.*)$' --output-na '' --sub-langs 'enUS,en' --user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0' -n --fragment-retries 'infinite' -N 6 --no-mtime --cookies 'cookies-funimation-com.txt' --extractor-args 'funimation:language=english'

Basically tricking yt-dlp into skipping PP on the first pass and calling another yt-dlp instance to do the PP of the previous file while the 1st instance continues on to the next. If PP is done externally with ffmpeg I don't see why multithreading is even needed outside of getting the console output. I'd be fine with an official solution that did just that even if it meant we couldn't see the final console output of the files.

Having PP done async is a major speed up for me (around 30 - 40%) since I'm downloading to a HDD.

There's more discussion about it here: https://discord.com/channels/807245652072857610/892817964716392500

@pukkandan
Copy link
Member

pukkandan commented Dec 7, 2021

youtube-dl was not written with any kind of multithreading in mind. Implementing this would need a rewrite of the overall architecture.

Since it would be difficult to keep track of multiple issues dealing with multithreading, we can use this issue to track all these:

(Adapted from: ytdl-org/youtube-dl#350 (comment))

  1. HTTP chunks Faster Downloads ytdl-org/youtube-dl#1498 - would only need changes to HttpFD (can be achived with aria2c too)
  2. Fragments - Already implemented as -N and HLS/DASH support for aria2c. This is not implemented for ISM yet [Feature Request]Add faster download for ism files #308
  3. Downloading video, audio subs and comments(?) in parallel - This is implemented when using --downloader ffmpeg (for streaming to stdout) and for [youtube] download live from the start, to the end #888. But no generalized implementation yet
  4. Separate downloading and PP (what this issue requests) - will need rewrite of entire workflow to implement directly. But once (5) is implemented, then this can piggy-back off it using locks
  5. Download entries in a playlist in parallel Multi-Threaded downloading for playlists ytdl-org/youtube-dl#350 - Should be easier than (4) to implement. The difficulties are mostly in handing states/errors (ie: how should autonumber/max-downloads/abort-on-error etc behave?) and how to handle outputs. If these are not concerns, it is not too hard to do with a third party wrapper
  6. Download different URLs that are passed to yt-dlp in parallel - similar to (5), but can be even more easily achieved using third party wrappers

PS: if someone is trying to implement threading, imo they should put their efforts into (5)

@Jules-A
Copy link
Contributor

Jules-A commented Dec 7, 2021

PS: if someone is trying to implement threading, imo they should put their efforts into (5)

Except (5) is not what I want, I only ever want to download 1 video at a time. I think (5) already exists in another fork afaik.
EDIT: https://github.com/tuxlovesyou/squid-dl

@pukkandan
Copy link
Member

pukkandan commented Dec 7, 2021

tuxlovesyou/squid-dl

That is not a fork, but a wrapper


@Jules-A As I mentioned in above comment, if (5) is implemented natively, (4) can be added on trivially.

Say the implementation of (5) is like this: There is a --total-threads T CLI argument and yt-dlp will at any time launch only a max of T threads.

Then, we can add another CLI arg --download-threads N. When a download starts, the thread acquires a lock. The lock will only ever be simultaneously issued to maximum N threads. When the download ends (but not postprocess), the thread releases the lock and continues PP. So by configuring N=1,T=2, you get (4)

This is why I personally think it is better to spend effort on implementing (5) rather than trying to do (4) directly

PS:

With this, you also get (6) for free by modifying yt_dlp to treat the multiple URLs passed from cmd similar to a playlist

@Saklad5
Copy link
Author

Saklad5 commented Dec 7, 2021

First off, multithreading is not synonymous with pipelining, and I’d rather you reverted the renaming.

Second, I am not proposing that postprocessing start before downloading is finished. The goal here is to treat each step as a blackbox, thereby avoiding any breaking changes to existing functionality.

If this can’t be done without rewriting the entire workflow, so be it. But I think it can, and I’d personally like to try.

@pukkandan
Copy link
Member

pukkandan commented Dec 7, 2021

First off, multithreading is not synonymous with pipelining,

You need multithreading to be able to run a PP and a download at the same time (which is what u are requesting, right?)

Second, I am not proposing that postprocessing start before downloading is finished

Yes, that was clear. As I understand, you want the download of the second video to start while PP of first is ongoing

If this can’t be done without rewriting the entire workflow, so be it. But I think it can, and I’d personally like to try.

Sure, you can try. I will help wherever I can, and PRs are ofc welcome.

In the OP, you mention that the UI is where you see difficulty with this. For that, you can simply have the PPs write to the screen normally (overwriting any progress bar above). This effectively makes it so all messages gets to the console, with the progress bar always staying at the bottom. This will be a bit problematic if/when #1720 is implemented, but we can cross that bridge when we get to it

PS: After thinking a bit more, this is not an ideal solution. It won't be clear from the logs what video is being downloaded and what is being Postprocessed. Perhaps it can be improved by prefixing all PP messages with the video id? Anyway, this should not be too big of a concern

@Jules-A
Copy link
Contributor

Jules-A commented Dec 7, 2021

You need multithreading to be able to run a PP and a download at the same time (which is what u are requesting, right?)

That's not exactly true since I'm already doing it with my current commands/config.

This is why I personally think it is better to spend effort on implementing (5) rather than trying to do (4) directly

Sure but rewriting everything seems like a large undertaking and might not ever get done whereas it probably wouldn't take anywhere near as long to just call ffmpeg similar to how it's done with exec for those who want faster downloads now and don't care about the jumbled console output.

@Saklad5
Copy link
Author

Saklad5 commented Dec 7, 2021

OK, this is the point where I’d like it to start downloading the next item.

info_dict = self.post_process(dl_filename, info_dict, files_to_move)

So, right now a list of URLs are just iterated over with a for-loop.

yt-dlp/yt_dlp/YoutubeDL.py

Lines 3042 to 3054 in ddd24c9

def download(self, url_list):
"""Download a given list of URLs."""
url_list = variadic(url_list) # Passing a single URL is a common mistake
outtmpl = self.outtmpl_dict['default']
if (len(url_list) > 1
and outtmpl != '-'
and '%' not in outtmpl
and self.params.get('max_downloads') != 1):
raise SameFileError(outtmpl)
for url in url_list:
self.__download_wrapper(self.extract_info)(
url, force_generic_extractor=self.params.get('force_generic_extractor', False))

What if that was instead treated as a stack? Each URL could be fully resolved before downloading commences (already possible using the right parameters), then the resulting ie_result stack could be passed through to the downloader, which would pop off the top item, download it, then recursively call itself with the remainder of the stack. That would have to be spun off into a child process or something similar, of course.

After that point, the function would need to block until nothing else is doing post-processing. That could be accomplished with a mutex. After everything is finished, you simply wait for the child process to complete, and then you’re done.

@Saklad5
Copy link
Author

Saklad5 commented Dec 7, 2021

Anything obviously wrong with that plan? There’s a bit of a race condition where a child process could lock the mutex before the parent, admittedly.

@pukkandan
Copy link
Member

Each URL could be fully resolved before downloading commences (already possible using the right parameters),

If you extract links long before you download them, they may expire

then the resulting ie_result stack could be passed through to the downloader, which would pop off the top item, download it, then recursively call itself with the remainder of the stack. That would have to be spun off into a child thread or something similar, of course.

Why? This makes no sense to me and has many issues - one being that you'll reach recursion limit quickly. It makes more sense to instead keep all downloads in the main thread and do the postprocessing in a separate thread.


First see how playlists are handled in the code. Your plan doesn't seem to account for them at all.

Also, keep in mind that extract_info must return the infodict after postprocessing

@Saklad5
Copy link
Author

Saklad5 commented Dec 7, 2021

If you extract links long before you download them, they may expire

Good point. In that case, extraction needs to be lumped together with downloading.

Why? This makes no sense to me and has many issues - one being that you'll reach recursion limit quickly. It makes more sense to instead keep all downloads in the main thread and do the postprocessing in a separate thread.

You’re right, I’m clearly too used to Swift’s reentrant concurrency. Python’s processes correspond to actual threads, so that’d result in a thread explosion.

I think it’d be better to keep the main thread clear if we aren’t bothering with recursion, though, if only to make potential UI improvements easier in the future.

Python’s multiprocessing queues are thread-safe and process-safe, right? What if there was an extracting/downloading queue and a postprocessing queue? The main process could populate the downloading queue with URLs, and the downloading process could add to the postprocessing queue after each download. Sentinel values could be used to mark the end of a queue, upon which the relevant process would close. When every process is closed, the main process can follow suit.

First see how playlists are handled in the code. Your plan doesn't seem to account for them at all.

It’s a bit hard for me to follow, honestly. I know ie_result can represent a playlist with an entries property, but I’m unclear on where that is iterated through.

@pukkandan
Copy link
Member

It’s a bit hard for me to follow, honestly. I know ie_result can represent a playlist with an entries property, but I’m unclear on where that is iterated through.

extract_info => extract data from URLs
process_ie_result => Processes the result and decides what to do based on whether it's video, redirect or playlist
__process_playlist => handles playlists,
process_video_result+process_info => handles individual videos

download and download_with_info_file are thin wrappers around extract_info/process_ie_result to make it easier to call from CLI

@mhogomchungu
Copy link

If i can chime in to the discussion, i propose the following:-

  1. yt-dlp continue to be a single threaded application and where everything is done sequentially.
  2. If somebody gives it 10 urls for example, the current behavior is to download them sequentially. yt-dlp should add a command line switch to enable concurrent downloading and it should handle it by spinning up user configurable number of processes and each process should handle its url sequentially(PP should follow after downloading as it is currently done).
  3. The yt-dlp process "manager" should capture output from from each subprocess and print them itself after prefixing it with something that uniquely identify each sub process to make it possible to tell what printed data came from what subprocess.
  4. The yt-dlp process "manager" should wait until all sub process are done before exiting.
  5. Ideally, the yt-dlp process "manager" should be bundled in and simply activated when the right switch is passed to yt-dlp or an additional tool that is managed by the project.

@pukkandan
Copy link
Member

@mhogomchungu the "yt-dlp process manager" sounds like it is better as a separate program. And in fact, programs like this already exist. See the above-mentioned https://github.com/tuxlovesyou/squid-dl. If we are doing this in yt-dlp itself, it must be done properly with support in the core program, not as some wrapper spawning processes

@mhogomchungu
Copy link

mhogomchungu commented Dec 8, 2021

My proposal implements concurrency through multiple processes and it needs a process manager.

Discussed proposals implements concurrency through multiple threads and it need a thread manager and this seems to be harder to do since "yt-dlp was not build with multi threading in mind".

One way or the other, a concurrency manager of some sort can not be avoided here and both approaches can be implemented in the core program.

The mentioned tool is doing exactly what i do in Media Downloader[1] when downloading playlists and it will have competition soon because i play to offer the functionality from CLI only.

[1] https://github.com/mhogomchungu/media-downloader

@Saklad5
Copy link
Author

Saklad5 commented Dec 8, 2021

@mhogomchungu

The mentioned tool is doing exactly what i do in Media Downloader[1] when downloading playlists and it will have competition soon because i play to offer the functionality from CLI only.

First off, this isn’t a competition.

Second, yt-dlp’s heavy use of dependency injection means it doesn’t actually share state that much, which should make it easier to retrofit for this form of concurrency. Furthermore, Python (like many modern languages) has built-in concurrency systems that can do a lot of the heavy lifting.

Third, I don’t think anyone has argued that this should be default behavior. Many existing options are likely incompatible with this form of processing, and having that many intermediate files at once may require more storage than users expect. Besides, default postprocessing is unlikely to take very long.

Finally, for the sake of avoiding complications with bandwidth throttling and CPU thrashing, I think it is best to assume that downloading saturates network bandwidth and postprocessing uses all available CPU resources. That is, neither concurrent downloading nor concurrent postprocessing are beneficial.

@Saklad5
Copy link
Author

Saklad5 commented Dec 8, 2021

Python’s multiprocessing library doesn’t seem to provide a deque, so playlists pose a bit of an issue. Until they are extracted/downloaded, they can’t be identified as playlists. Once they are, they’ll have to add each entry to the end of the download queue, unfortunately meaning non-playlists get priority over playlists.

For the sake of not complicating things considerably, it would probably be best to make the options to write playlist data to a file incompatible with pipelining. That could be revisited in the future.

@pukkandan Is it safe to assume that playlist entries don’t become stale?

@pukkandan
Copy link
Member

pukkandan commented Dec 8, 2021

I did say it was going to be difficult to implement 🤷

If you don't want to write full integration with all features of yt-dlp, you are better off writing a wrapper. See https://github.com/yt-dlp/yt-dlp#embedding-yt-dlp. That way, you can neatly separate the download and PPs without worrying about the features that you never use. For just a single video, the code could be as simple as this:

import yt_dlp

def download(url):
    with yt_dlp.YoutubeDL({
                'format': 'bv,ba',
                'outtmpl': '%(title)s [%(id)s].%(format_id)s.%(ext)s',
                'fixup': 'never',
            }) as ydl:
        return ydl.sanitize_info(ydl.extract_info(url))

def postprocess(info):
    with yt_dlp.YoutubeDL({
                'format': 'bv+ba',
                'outtmpl': '%(title)s [%(id)s].%(ext)s',
                'fixup': 'force',
                'postprocessors': [{'key': 'FFmpegMetadata'}],
            }) as ydl:
        return ydl.process_ie_result(info)

info = download(URL)
postprocess(info)  # Run this in a separate thread/process

For batch files/playlists, you can enumerate through the data similar to squid-dl

Is it safe to assume that playlist entries don’t become stale?

I don't understand what you mean

@mhogomchungu
Copy link

First off, this isn’t a competition.

It was meant to show experience with doing process based concurrent downloading with yt-dlp. When using my app, i typically run multiple instances of yt-dlp and each instance is configured to use aria2c to get multiple connections per instance for faster downloads. Again, i am mentioning this to show experience with concurrent downloading with yt-dlp and not to brag or competing with anybody.

Third, I don’t think anyone has argued that this should be default behavior.

It should not be the default and when/if it is implemented then as a GUI frontend developer to yt-dlp, my biggest interest is in how the output will be produced and how i can tell how many instances are running and what output belong to what instance.

@dimitarvp
Copy link

While I'd love having sequential downloads and async postprocessing (happening off the main downloader thread/process) I see the following difficulties:

  1. A lot of this behaviour seems to be downloader-specific. Hard to generalize in a rewrite.
  2. The UI will suffer a lot. Such a feature would likely open a huge can of worms that will involve writing an ncurses UI. The maintainers are likely not up for such a big undertaking, especially having in mind that wrappers exist and you can go most of the way to this desired feature through them.
  3. Indeed, pre-populating links can easily become a problem because YouTube (at least) uses expiring links.

IMO the way to have this today is to parallelize / embed yt-dlp and maybe limit download speed so you don't have e.g. 10 downloads each taking forever, and getting the added advantage of the single-threaded nature of the post-processing code since after each download finishes it'll occupy a single CPU core.

I pondered scripting this in bash / zsh by having a global download limit value in an environment variable and then divide that by the CPU cores and then spawn N independent yt-dlp processes. You can also use multitail to spawn all of them so you can check the output of each in real time.

So while I would love this I can see how many complexities it can unleash and how the maintainers wouldn't think it's worth it.

@Saklad5
Copy link
Author

Saklad5 commented Dec 17, 2021

I’m inclined to agree, particularly if it is unacceptable to lack compatibility with particularly thorny configurations.

If it were possible to cleanly separate post-processing from downloading (such that the Python interface could call the latter using the output of the former without bespoke configuration tweaking), this would be quite doable. It’d still screw up the UI, though.

Since that doesn’t seem to be the case, I think this issue should be closed at the maintainer’s discretion.

@pukkandan
Copy link
Member

As I said above, I intend to leave this issue open to hold all future discussions on multi-threading

@fstirlitz
Copy link
Contributor

One of the reasons I originally filed #267 was that I hoped that moving to Python 3 might allow us to take advantage of the language’s native asynchronous programming support to implement simultaneous downloading of multiple streams (maybe even livestreams) in a performant manner. But there are some obstacles to going down that route:

  • It would require re-writing all downloaders.
  • It might necessitate changes to the public API this project exposes as a Python module. (Though I believe it would be possible to maintain a backwards-compatible interface.) Something of a related issue is that this project doesn’t really have a well-defined stable API surface in the first place.
  • Our HTTP client library, urllib, is synchronous. Since yt-dlp only supports Python ≥ 3.6, we could add e.g. aiohttp as a dependency, which supports the same Python versions. However, upstream has been historically pretty reluctant to take up hard dependencies beyond the Python standard library, and so far this project has maintained this stance.

@iuriguilherme
Copy link

In my experience using ytdl with fully async programs is that the actually downloading is what blocks the whole thread because it depends on the remote server upload speed (as in g00g13 for example).

So if aiohttp can be optionally used instead of urlib3/requests, it already solves the major bottleneck for batch processing.

@fstirlitz
Copy link
Contributor

The only thing that may be complicated by this proposal, then, is the UI. At worst, this could be dealt with by simply not displaying progress when run in this manner.

I don’t think it’s even that hard to do readable progress reporting of concurrent tasks… assuming you manage the concurrency part well enough in the first place.

Code sample
#!/usr/bin/env python3
import sys


class TTYProgressMsg:
	def __init__(self, printer, items):
		self.__data = items
		self.__i = None
		self.__printer = printer

	def _print(self):
		self.__printer._print(self.__data)

	def __enter__(self):
		self.__printer._append(self)
		return self

	def __exit__(self, *exc):
		if exc != (None, None, None):
			return
		self.__printer._remove(self)

	def update(self, *items):
		self.__data = items
		self.__printer._refresh()


class TTYPrinter:
	def __init__(self):
		self.__progress_msgs = []

	def __kill_line(self):
		sys.stdout.write('\r\x1B[2K')

	def _print(self, items):
		self.__kill_line()
		for item in items:
			sys.stdout.write(item)
		sys.stdout.write('\n')

	def __back_out(self):
		l = len(self.__progress_msgs)
		if l == 0:
			return
		sys.stdout.write(f'\x1B[{l}F')

	def _append(self, progress):
		self.__progress_msgs.append(progress)
		progress._print()
		return progress

	def _remove(self, progress):
		self.__back_out()
		self.__progress_msgs.remove(progress)
		progress._print()
		for prog in self.__progress_msgs:
			prog._print()
		sys.stdout.flush()

	def _refresh(self):
		self.__back_out()
		for progress in self.__progress_msgs:
			progress._print()
		sys.stdout.flush()

	def event_msg(self, *items, interrupt=True):
		if not interrupt:
			self.__back_out()
		self._print(items)
		for prog in self.__progress_msgs:
			prog._print()
		sys.stdout.flush()

	def progress_msg(self, *items):
		return TTYProgressMsg(self, items)


if __name__ == '__main__':
	import random
	import asyncio

	prt = TTYPrinter()

	running = 0

	async def task(id, d):
		global running
		perc = 0
		running += 1
		with prt.progress_msg(f'[{id}] ?') as progress:
			while perc < 100:
				progress.update(f'[{id}] {perc}%')
				await asyncio.sleep(d)
				perc += random.randint(0, min(10, 100 - perc))
			progress.update(f'done {id}')
		running -= 1

	async def random_events():
		evt = 0
		await asyncio.sleep(0)
		while running:
			await asyncio.sleep(random.uniform(0.2, 5))
			prt.event_msg(f'random event {evt}', interrupt=True)
			evt += 1

	async def main():
		await asyncio.gather(
			random_events(),
			task('task1', 0.2),
			task('task2', 0.1),
			task('task3', 0.3),
			task('task4', 0.15),
		)

	asyncio.run(main())

	prt.event_msg('all done')

@Grub4K
Copy link
Member

Grub4K commented Dec 4, 2022

I don’t think it’s even that hard to do readable progress reporting of concurrent tasks… assuming you manage the concurrency part well enough in the first place.

#5680 now supports parallel progress output automatically.

Implementation idea for parallel postprocessing

__process_iterable_entry just calls process_ie_result, which has the postprocessing step already embedded. I have the assumption that ie_result = self.run_all_pps('playlist', ie_result) needs a fully postprocessed playlist to not break. I also only consider postprocessing since pre_process are going to be very hard to parallelize.

Implementation

If we were to create a process_ie_result which does not do postprocessing, then parallelism could be applied in the for loop inside __process_playlist using a separate thread queue for postprocessing.
We first download using the process_ie_result without postprocessing, then enqueue this item and wait for queue completion before the self.run_all_pps call. The queue is a global one provided through either YoutubeDL or some global concurrency module (seems cleaner).

That means most of the public wrappers would need either a flag post_process=False or private versions which skip postprocessing. For process_video_result this could be done by skipping info_dict = self.run_all_pps('after_video', info_dict).

This might need some refactoring to pass down which sort of processing needs to be applied (could be calculated from the info dict again). It would then be handled inside __process_iterable_entry.

Downsides

  • Would handle nested playlists separately, so [[0, 1], [2, 3]] waits on [0, 1] before doing [2, 3].
  • Only works for postprocessing.

@fillwithjoy1
Copy link

To get around this issue I've been using this command: yt-dlp -v "https://www.funimation.com/shows/spice-and-wolf/" --config-location funimation.conf --exec "start /B yt-dlp --config-location funimation.conf -q --fixup force --embed-subs --load-info-json %%(__infojson_filename)q" --write-subs --download-archive archive.txt --ffmpeg-location "D:\dummy" --write-info-json -P "D:\Temp"

conf: -f '(bv*+ba/b)[format_note=Uncut] / (bv*+ba/b)' -o '%(extractor)s\%(title)s%(myindex)s.%(ext)s' -P 'D:\Videos' -P 'temp:D:\Temp' --parse-metadata 'original_url:#%(playlist_index)s' --parse-metadata ' - %(playlist_index)d:^(?P<myindex> - \d+)$' --parse-metadata '%(series)s - S%(season_number)sE%(episode_number)s - %(episode)s:^(?P<title>.+ - S\d+E\d+ - \S+.*)$' --output-na '' --sub-langs 'enUS,en' --user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0' -n --fragment-retries 'infinite' -N 6 --no-mtime --cookies 'cookies-funimation-com.txt' --extractor-args 'funimation:language=english'

Basically tricking yt-dlp into skipping PP on the first pass and calling another yt-dlp instance to do the PP of the previous file while the 1st instance continues on to the next. If PP is done externally with ffmpeg I don't see why multithreading is even needed outside of getting the console output. I'd be fine with an official solution that did just that even if it meant we couldn't see the final console output of the files.

Having PP done async is a major speed up for me (around 30 - 40%) since I'm downloading to a HDD.

There's more discussion about it here: https://discord.com/channels/807245652072857610/892817964716392500

Hey do you still have the command working? Just wanting to get it working on my end currently

@Jules-A
Copy link
Contributor

Jules-A commented Aug 17, 2023

Hey do you still have the command working? Just wanting to get it working on my end currently

Yeah but I think I had to change a few things around --parse-metadata but that has nothing to do with parallel muxing/downloading and rather just a personal change for title. I also moved to using aliases so it looks a bit different now and changed my batch files to fix archive breaking when closing the download window when a video was muxing.

So my full config looks like:
base.conf

-o '%(extractor)s\%(title).170s%(myindex)s.%(ext)s'
-P 'E:\Videos'
-P 'temp:E:\Temp'
--parse-metadata 'original_url:##%(playlist_index)s'
--parse-metadata ' - %(playlist_index)d:^(?P<myindex> - \d+)$'
--parse-metadata '%(series)s - S%(season_number)dE%(episode_number)d - %(episode)s:^(?P<title>.+ - S\d+E\d+ - \S+.*)$'
--output-na ''
--sub-langs 'enUS,en,en-US'
--user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0'
--fragment-retries '200'
-R '200'
-N '6'
--no-clean-info-json
--no-mtime
--retry-sleep 'http,fragment,file_access:linear=0.5:10:0.2'
--alias archive-location,--Xl '--download-archive "D:\Programs\YoutubeDL\archive.txt"'
--alias common-code,--X0 '--download-archive unprocessed-archive.txt --ffmpeg-location "D:\dummy" {1} --write-info-json -P "E:\Temp" --exec "start /B yt-dlp -q --config-location {0} --Xl --fixup force {2} --load-info-json %(__infojson_filename)q"'
--alias with-subs,--Xs '--X0 {0} --write-subs --embed-subs'
--alias without-subs,--Xn '--X0 {0} --no-write-subs --no-embed-subs'

generic.conf

--config-location 'base.conf'
-N 12

and called with Generic Download.bat

copy /y archive.txt unprocessed-archive.txt
yt-dlp -v "https://www.youtube.com/watch?v=iN0-dRNsmRM" --config-location "generic.conf" --Xs "generic.conf"
pause

Don't have CR sub atm so didn't post my CR configs as I think there's some ongoing issues with the CR extractor right now anyway that might need changing it.

EDIT: Might as well post it but here's my crunchyroll.conf

--config-location 'base.conf'
-f '(bv*+ba/b)'
-n
--cookies cookies-crunchyroll-com.txt
--extractor-args 'crunchyrollbeta:hardsub=None,enUS,en,en-US;language=en-US'

@fillwithjoy1
Copy link

Hey do you still have the command working? Just wanting to get it working on my end currently

Yeah but I think I had to change a few things around --parse-metadata but that has nothing to do with parallel muxing/downloading and rather just a personal change for title. I also moved to using aliases so it looks a bit different now and changed my batch files to fix archive breaking when closing the download window when a video was muxing.

So my full config looks like: base.conf

-o '%(extractor)s\%(title).170s%(myindex)s.%(ext)s'
-P 'E:\Videos'
-P 'temp:E:\Temp'
--parse-metadata 'original_url:##%(playlist_index)s'
--parse-metadata ' - %(playlist_index)d:^(?P<myindex> - \d+)$'
--parse-metadata '%(series)s - S%(season_number)dE%(episode_number)d - %(episode)s:^(?P<title>.+ - S\d+E\d+ - \S+.*)$'
--output-na ''
--sub-langs 'enUS,en,en-US'
--user-agent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0'
--fragment-retries '200'
-R '200'
-N '6'
--no-clean-info-json
--no-mtime
--retry-sleep 'http,fragment,file_access:linear=0.5:10:0.2'
--alias archive-location,--Xl '--download-archive "D:\Programs\YoutubeDL\archive.txt"'
--alias common-code,--X0 '--download-archive unprocessed-archive.txt --ffmpeg-location "D:\dummy" {1} --write-info-json -P "E:\Temp" --exec "start /B yt-dlp -q --config-location {0} --Xl --fixup force {2} --load-info-json %(__infojson_filename)q"'
--alias with-subs,--Xs '--X0 {0} --write-subs --embed-subs'
--alias without-subs,--Xn '--X0 {0} --no-write-subs --no-embed-subs'

generic.conf

--config-location 'base.conf'
-N 12

and called with Generic Download.bat

copy /y archive.txt unprocessed-archive.txt
yt-dlp -v "https://www.youtube.com/watch?v=iN0-dRNsmRM" --config-location "generic.conf" --Xs "generic.conf"
pause

Don't have CR sub atm so didn't post my CR configs as I think there's some ongoing issues with the CR extractor right now anyway that might need changing it.

EDIT: Might as well post it but here's my crunchyroll.conf

--config-location 'base.conf'
-f '(bv*+ba/b)'
-n
--cookies cookies-crunchyroll-com.txt
--extractor-args 'crunchyrollbeta:hardsub=None,enUS,en,en-US;language=en-US'

hmm, i'm trying to modify my command to download music in parallel processing but I'm kinda stuck

yt-dlp -o "%(artist)s - %(title)s.%(ext)s" -x --audio-format aac --add-metadata -f "ba" --embed-thumbnail --convert-thumb png --ppa "ThumbnailsConvertor+ffmpeg_o:-c:v png -vf crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\""

@Jules-A
Copy link
Contributor

Jules-A commented Sep 4, 2023

hmm, i'm trying to modify my command to download music in parallel processing but I'm kinda stuck

yt-dlp -o "%(artist)s - %(title)s.%(ext)s" -x --audio-format aac --add-metadata -f "ba" --embed-thumbnail --convert-thumb png --ppa "ThumbnailsConvertor+ffmpeg_o:-c:v png -vf crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\""

Converting audio, embedding thumbnail, converting thumbnail, cropping and I think adding metadata all requires ffmpeg (maybe embedding thumbnails uses another library, not sure.
Basically you'll want to do just downloading in the original process and then call another yt-dlp process to embed/process the downloaded files. I'm not sure about --ppa but I think that blocks the download, if it's done in another process it won't ofc.

So if you were to add it based on my config, that would be in the common-code alias, in the parameters of the 2nd process call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants