Media Downloader in Next.js, tailwind, Flask&sqlAlchemy
Tags: nextjs, python, tailwindcss, beginners
Published: 2023-07-25 07:52:36

It sounds easy, right? 🤔 Just take the link from the user, do some stuff on the backend, and serve them the file. 🎉 In theory, everything sounds easy. Even 6-hour video tutorials on how to build a Twitter/Instagram or Spotify clone sound easy...🙄

But then you close the tutorial you were watching and try to do it yourself, only to discover that you have trouble even setting up the configurations... ☝🤓

What I'm trying to say is that <u>beginnings are hard</u>, man.

- **Link:** [https://scrappy.tricksofthe.trade/](https://scrappy.tricksofthe.trade/)
- **Github:** [BorisKlco/nextjs.videoDownloader](https://github.com/BorisKlco/nextjs.videoDownloader)

I'm sure a more experienced person could rewrite this **much** better, but this is my first project, and I'm happy with what I learned. I learned about **Next.js**'s new App Router options, how to run a **Flask API**, and how to store data with **SQLAlchemy**.

First, I created a draft template in **Figma** to visualize how I wanted the website to look. My goal was to create a simple design, as I believe that simplicity is king. Many websites that are designed for downloading media are bloated with unnecessary content, which is often done for the sake of SEO. However, I believe that a simple design is more user-friendly..

## Backend and Flask API

I will use **yt-dlp**, a **youtube-dl** fork with **Python** support. To obtain information about the posted link and provide users with options to download media from it.

After user submits the form data to backend endpoint, I will use the **yt-dlp** class `.extract_info` to get a `JSON object` with all the information about the video, including the title, date, thumbnail, and formats.

```
@app.route("/extract_info", methods=["POST"])
def post_info():
    data = request.json
    with YoutubeDL() as ydl:
        try:
           info = ydl.extract_info(data, download=False)

               #Storing visit to db
               save_view = History(
                    title=info["title"],
                    image=info["thumbnail"],
                    link=info["original_url"],
                )
                db.session.add(save_view)
                db.session.commit()

           return jsonify(ydl.sanitize_info(info))
        except:
            return {"error": "Wrong URL"}
```

Another **Flask** endpoint will be `/get_me_link`, which will download selected media and provide user with a link to download it.

```
@app.route("/get_me_link", methods=["POST"])
def file_download():
    data = request.json
    file = download.get_file(data["url"], data["type"])
    return {"url": "https://url/serve_file/" + file}


@app.route("/serve_file/<file>")
def serve_file(file):
    path = os.getcwd()
    download_file = path + "/files/" + file
    return send_file(download_file, as_attachment=True)
```

Last endpoit is for serving data from db

```
@app.route("/history", methods=["GET"])
def history():
    db_query = History.query.order_by(History.id.desc()).all()
    result = []
    for row in db_query:
        result.append(
            {"id": row.id, 
             "title": row.title, 
             "image": row.image, 
             "link": row.link})
    return jsonify(result)
```

## Next.js layout:

![Next.js layout visualization](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mnlqg2kar9quctd8e451.png)

## `<Search />`

After submitting the link, perform a basic link check to ensure that it is supported.

```
const supportedSites = [
    "youtu","tiktok","instagram","twitch","twitter","reddit",
  ];
function checkSupported(userInput: String) {
    return supportedSites.some((item) => userInput.includes(item));
  }
```

If site is supported fetch data with **react-query** and render `<Result />`

```
        {result.isFetching ? (
          <div className="flex justify-center items-center">
            <h1 className="truncate my-4 text-xl sm:text-3xl">
              LoAd1nG.. SeRvEr go Brrr..
            </h1>
            <Image
              src="/images/search/brr.svg"
              height={48}
              width={48}
              className="object-fit sm:pl-2"
              alt="Loading"
            />
          </div>
        ) : result.isFetched ? (
          <div>
            <Result data={resultData} />
          </div>
        ) : null}
```

## `<Result />`

Check if data.error property exists. If it does not, return a nice representation of this data in divs, Link and Image elements.

At this point, we can also perform additional checks, such as if  video is too long or if video is live streamed.

If everything is _Gucci_ show user `<Download />` component. 

```
<Download url={data.original_url} type="audio" format="MP3" />
<Download url={data.original_url} type="video" format="MP4" />
```

## `<Download />`

The download component is using `react-query` to fetch data after user clicks on it. The component will have three additional states: `.isFetching`, `.isFetched`, and `none`. The `.isFetching` state will be used to show a spinner wheel while the data is being fetched. The `.isFetched` state will be used to show a link to download the file. The `none` state will be used to show the download option.

```
{link.isFetching ? 
      (<button>
          <AiOutlineLoading className="animate-spin" />
        </button>) 
: link.isFetched ? 
     (<a href={linkData.url}> Download <Image/> </a>) 
: (
        <button
          onClick={() => {link.refetch();
            toast.success("Preparing it for uWu.. 🐱")}>
            {format}
        </button>
   )}
```

## Next.js App Router `error.tsx` and `loading.tsx`

in **app dir** I created error handler `error.tsx`
- must be `'use client'`
- you have optional props `{error, reset}` ( TS: `{ error: Error; reset: () => void }` )

```
const Error = ({ error, reset }: { error: Error; reset: () => void }) => {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <div>
        <Link href={"/"}>
          Go back home
        </Link>
      </div>
    </div>
  );
};
```

For the _/history_ route, I use `loader.tsx` to create a skeleton if fetching data takes longer than expected. This skeleton will create a list of made-up items that look like the data after it has been fetched.

```
export default async function Loading() {
  return (
    <>
      <div className="mt-6 md:mt-8 flex flex-wrap">
        {Array.from({ length: 12 }, (movie, i) => (
          <div className="max-w-[16rem]animate-pulse" key={i}>
            <p className="truncate mb-2 invisible">Loading</p>
            <div className="animate-pulse"></div>
          </div>
        ))}
      </div>
    </>
  );
}
```

## Thank you for reading ♥

- **Link:** [https://scrappy.tricksofthe.trade/](https://scrappy.tricksofthe.trade/)
- **Github:** [BorisKlco/nextjs.videoDownloader](https://github.com/BorisKlco/nextjs.videoDownloader)