Skip to content

Instantly share code, notes, and snippets.

@smoqadam

smoqadam/blog.md Secret

Created April 19, 2020 06:37
Show Gist options
  • Save smoqadam/96237548549508db60475d018d55c1f6 to your computer and use it in GitHub Desktop.
Save smoqadam/96237548549508db60475d018d55c1f6 to your computer and use it in GitHub Desktop.

PHP is not the best language for writing a download manager, but in this article, we are going to learn how we can download videos from Youtube. Because PHP does not support multithreading, we cannot make a proper download manager that split the file into several chunks and download it concurrently. There are some ways to add multithreading to PHP, but for the sake of this article, we won't go through those ways and will stick to single threading.

At the end of this article, we have a command called dl, and it gets a Youtube video URL to download.

https://gist.github.com/66e4f4e4cee3e02f72fc79ba2e4927db

What we will learn by the end of this article:

  • How to create a command-line application
  • How to use the Factory design pattern
  • How to get information about a Youtube video such as formats, titles, etc.
  • How to download a file with PHP with a progress bar

Create the project

create a new directory and run composer init inside it:

https://gist.github.com/20685d4a22ca6e4b7f592809484059f4

After running composer init‍ you need to answer a few questions. This command creates a composer.json file inside youtube-downloader directory.

Install dependencies

https://gist.github.com/818541b8f1e8a638ebd936fb72497def

  • symfony/console is a great package that helps us to create a command-line application and make it easier to handle input and outputs.

  • smoqadam/youtube-video-info gives us information about a Youtube video. The information such as video formats, subtitles, download URL, title, etc.

autoloading

Create a directory and call it src we will put all of our classes inside this directory. For taking advantage of composer autloading, open composer.json and add autoload sectoin such as below:

https://gist.github.com/4f6c9fa8c1d411b43256e892a3f5d813

Downloader is our chosen namespace for this project, and whenever the composer wants to load a class within this namespace, it looks inside the src directory.

You can find more information about PSR4 here.

Handle Command Line arguments

Create a Download.php file inside the src/Commands directory and with the following content:

https://gist.github.com/49de74d6240c632b0dace803fea594b1

Download class extended Command from the symfony/console package, which gives us many features to handle command-line input and outputs.

  • $defaultName is the name of our command.
  • configure method is using to config our command, such as add description, add arguments, options, and so on.
  • Inside the configure method, we can configure the dl command, for example, add a description, add arguments, options, and many more.
  • And finally, execute will call when we run the dl command from CLI. It receives two parameters, $input for handling the inputs such as options and arguments, and $output for managing the output. As you can see, we used <info></info> tags in the write method to colorize the output.

Let's make our dl command work by creating a index.php file under root directory:

https://gist.github.com/798e0a1fdeaaefc045ee1c6efa7a2573

Application is the main class responsible for handling the command line application, so to make our dl command work, we must use the add method to pass a new instance of Download class to it.

Now, open your terminal and run the following command:

https://gist.github.com/3a86cd418d9bc82c8230c7a7954e820f

you should see http://example.com in green color as output.

Downloader

In this section, we are going to make a downloader class. This class receives a URL and a file name, and it will download the file for us. But before we create our Downloader class, let's think about how we can make it extendable? By extendable, I mean how we can have a Downloader that downloads files from different sources such as Youtube or Vimeo videos or may a direct link.

We know that Youtube videos have different qualities, and each quality has a different download link. So before we can download a video, we must get all qualities and ask users which quality they want to download. But this process will be different if they're going to download from Vimeo or a direct link.

In the SOLID principle, S is for Single responsibility, which means each class must do only one thing. With that explanation, we need to have different classes for different sources.

To achieve this, we can use Factory design pattern to instantiate a new object based on the URL that the user wants to download.

For example, if it was a Youtube URL, it will return a new object from Youtube class and so on.

Factory.php class under src/ directory would be something like this:

https://gist.github.com/745f53d35c552a7960cce6a234311add

Factory Design pattern is creational pattern that create a different object based on some conditions. In our case, we checked the $url and if it was a Yoututbe link, it returns a new instance of Yoututbe class, otherwise it throws an Exception.

Handle different sources

As we saw earlier, we decided to let our app download from different sources, but these sources or providers must follow some rules to prevent chaos and make it easier to extend and debug. We define our rules inside ProviderAbstract, and every provider (e.g., Youtube class) must extend from this class, and they must implement the init method.

ProviderAbstract

https://gist.github.com/2611dcf8ad0baf98f6212837d00f5e39

Every provider must initialize and prepare the download link and output file name inside the init method.

Youtube provider

Create a directory under src and call it Providers then create a file Youtube.php inside it:

https://gist.github.com/e5df53a416885a7e57364d6a2eb0a2cb

Youtube class extended from ProviderAbstract, which means it must implement the init method. Inside init, we took advantage of smoqadam/youtube-video-info package to parse a youtube video. Then created a simple menu with each format in a new line. readline function receives an input which in our case it's an index of a menu item and whenever the user enters the number associated to the format, we filled up $this->url and $this->name.

The menu would be something like this:

https://gist.github.com/bc2500a9fa5ef92b287073ac313c9f33

| Note: File sizes are in byte, you can convert to MB by yourself.

Downloader

Here we are going to see how we can download a file and how to show a progress bar with PHP. Create a file under the src directory and call it Downloader.php:

https://gist.github.com/d7c1c34f2f603f7f4c6feb2603121186

This class is not as scary as it looks. Just notice how we passed a ProviderAbstract class to constructor. We will talk about the download method in the next section.

What is Stream Context?

In PHP, Stream Context is using to enhance or modify a stream, and it can pass to most file handling functions. From the PHP website:

A context is a set of parameters and wrapper specific options which modify or enhance the behavior of a stream. Contexts are created using stream_context_create() and can be passed to most filesystem related stream creation functions (i.e. fopen(), file(), file_get_contents(), etc...).

stream_context_set_params receives a stream context and an array of parameters. One of the parameters we can set is notification that is a function. This function will be called whenever our context state such changes. (see more: https://www.php.net/manual/en/function.stream-notification-callback.php)

We passed the URL we want to download and the context we made earlier to fopen, it will open the remote file in a read mode, and whenever the state of the file changes, it will call streamCallback method. Inside the streamCallback method, we checked for the notification type and printed the proper information about each notification.

Now open sr/Command/Download.php and change the execute method as below:

https://gist.github.com/0a361701c7c18ce1c6c2ce6bbd19ac90

Then open your terminal and try to download a youtube video:

https://gist.github.com/379e14dd2483a874352ff4b53670a0ae

Cool, right?

Download from a direct link

So far, we can download youtube videos, but what about direct links such as http://example.com/file.mp3?

Because we used the Factory design pattern and Single Responsibility principle, it would be as easy as create a separate class for direct links.

Create Direct.php file under src/Providers directory with the following contents:

https://gist.github.com/7210698f27da59db20e145876f2b8880

Because direct links are direct, the only thing we should do is to set $this->url and $this->name. Now, open src/Factory.php and change it as below:

https://gist.github.com/932ffb30e01508d79bda613078481dc5

As you can see, we changed the default section to return a new object of Direct class.

That's all we must do if we want to handle other sources.

Summary

Although PHP is not an excellent tool to write a downloader, we saw how we could manage and download a file from Youtube or a direct link. We learned about Stream Context and how we can use them of change or get information about a stream. We used the Factory design pattern and saw what the heck Single Responsibility means. Now it's your time to use your imagination and creativity to extend this project. Maybe by adding more providers or how to make use of multithreading extensions such as https://github.com/krakjoe/parallel to make it more usable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment