Media Downloader in Next.js, tailwind, Flask&sqlAlchemy

borisklco

BorisKlco

Posted on July 25, 2023

Media Downloader in Next.js, tailwind, Flask&sqlAlchemy

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 beginnings are hard, man.

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"}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Next.js layout:

Next.js layout visualization

<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));
  }
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

<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" />
Enter fullscreen mode Exit fullscreen mode

<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>
   )}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Thank you for reading ♥

💖 💪 🙅 🚩
borisklco
BorisKlco

Posted on July 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related