Media Downloader in Next.js, tailwind, Flask&sqlAlchemy
BorisKlco
Posted on July 25, 2023
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"}
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:
<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 ♥
Posted on July 25, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.