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

[Video Downloader] Unlock its full ability #16845

Merged
merged 7 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions extensions/youtube-downloader/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
# YouTube Downloader Changelog
# Video Downloader Changelog

## [Improvement] - 2025-02-17
## [Improvement] - {PR_MERGE_DATE}

- Add an experimental preference option for forcing IPv4 to solve some network issues
- Add a message to remind users not to close the current window while installing homebrew packages

## [Enhancements] - 2025-02-17

- Unlock its full ability from all sites
- Move the warning message to the form description
- Only show download failed message on errors
- Fix live video condition
- Add a link accessory to the form view to show the supported sites
- Mention the `yt-dlp` in readme
- Mention supported sites in readme
- Comment `Can I download clips from YouTube` out since we don't support it yet
- Update screenshots since the format selector is not ready yet

## [Improvement] - 2025-02-15

- Add a preference option for toggling read URL from clipboard support
Expand Down
19 changes: 13 additions & 6 deletions extensions/youtube-downloader/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
# Video Downloader

> Download high-quality YouTube videos with Raycast.
> Download high-quality videos from YouTube and other platforms with Raycast.

![youtube-downloader-1.png](metadata%2Fyoutube-downloader-1.png)

## Installation

To use this extension, you must have `ffmpeg` installed on your machine.
To use this extension, you must have `yt-dlp` and `ffmpeg` installed on your machine.

The easiest way to install this is using [Homebrew](https://brew.sh/). After you have Homebrew installed, run the
following command in your terminal:

```bash
brew install ffmpeg
brew install yt-dlp ffmpeg
```

Depending on your macOS version, the package might be located in a different path than the one set by the extension. To
Expand All @@ -26,15 +27,21 @@ Then, update the path in the extension preferences to match the output of the ab
You'll also need `ffprobe`, which is usually installed with `ffmpeg`. Just run `which ffprobe` and update the path
accordingly.

## Supported Sites

See <https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md>.

## **FAQs**

### **Is there a YouTube downloader that actually works?**
### **Is there a YouTube downloader that actually works?**

Yes, Raycast's YouTube Downloader is consistently updated to ensure reliable functionality.
Yes, Raycast's Video Downloader is consistently updated to ensure reliable functionality.

### **Can I download clips from YouTube?**
<!--
### **Can I download clips from YouTube?**

Absolutely\! Our extension supports downloading full videos, clips, and even YouTube Shorts.
-->

### **How do I download a YouTube video with a manipulated URL?**

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
13 changes: 13 additions & 0 deletions extensions/youtube-downloader/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 11 additions & 4 deletions extensions/youtube-downloader/package.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"$schema": "https://www.raycast.com/schemas/extension.json",
"name": "youtube-downloader",
"title": "YouTube Downloader",
"description": "Download YouTube video from Raycast",
"title": "Video Downloader",
"description": "Download videos from YouTube, Twitter, Twitch, Instagram and more using yt-dlp CLI",
"type": "module",
"icon": "youtube.png",
"author": "vimtor",
Expand All @@ -19,13 +19,19 @@
"Data",
"Productivity"
],
"keywords": [
"video",
"downloader",
"youtube",
"yt-dlp"
],
"license": "MIT",
"commands": [
{
"name": "index",
"title": "Download Video",
"subtitle": "YouTube",
"description": "Download YouTube video with parameters",
"subtitle": "Video Downloader",
"description": "Download video with parameters",
"mode": "view"
}
],
Expand Down Expand Up @@ -103,6 +109,7 @@
"@raycast/api": "^1.89.1",
"@raycast/utils": "^1.18.1",
"date-fns": "^4.1.0",
"is-url-superb": "^6.1.0",
"nano-spawn": "^0.2.0",
"pretty-bytes": "^6.1.1"
},
Expand Down
33 changes: 24 additions & 9 deletions extensions/youtube-downloader/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ import {
import { useEffect, useMemo, useState } from "react";
import { useForm, usePromise } from "@raycast/utils";
import nanoSpawn from "nano-spawn";
import { DownloadOptions, isValidHHMM, isYouTubeURL, parseHHMM, preferences } from "./utils.js";
import { DownloadOptions, isValidHHMM, isValidUrl, parseHHMM, preferences } from "./utils.js";

export default function DownloadVideo() {
const [error, setError] = useState(0);
const [warning, setWarning] = useState("");

const { handleSubmit, values, itemProps, setValue, setValidationError } = useForm<DownloadOptions>({
initialValues: {
Expand Down Expand Up @@ -80,8 +81,14 @@ export default function DownloadVideo() {
const line = data.toString();
console.error(line);

toast.title = "Download Failed";
toast.style = Toast.Style.Failure;
if (line.startsWith("WARNING:")) {
setWarning(line);
}

if (line.startsWith("ERROR:")) {
toast.title = "Download Failed";
toast.style = Toast.Style.Failure;
}
toast.message = line;
});

Expand Down Expand Up @@ -118,7 +125,7 @@ export default function DownloadVideo() {
if (!value) {
return "URL is required";
}
if (!isYouTubeURL(value)) {
if (!isValidUrl(value)) {
return "Invalid URL";
}
},
Expand All @@ -145,7 +152,7 @@ export default function DownloadVideo() {
const { data: video, isLoading } = usePromise(
async (url) => {
if (!url) return;
if (!isYouTubeURL(url)) return;
if (!isValidUrl(url)) return;

const result = await nanoSpawn(
preferences.ytdlPath,
Expand Down Expand Up @@ -180,7 +187,7 @@ export default function DownloadVideo() {

useEffect(() => {
if (video) {
if (video.live_status !== "not_live") {
if (video.live_status !== "not_live" && video.live_status !== undefined) {
setValidationError("url", "Live streams are not supported");
}
}
Expand All @@ -190,7 +197,7 @@ export default function DownloadVideo() {
(async () => {
if (preferences.autoLoadUrlFromClipboard) {
const clipboardText = await Clipboard.readText();
if (clipboardText && isYouTubeURL(clipboardText)) {
if (clipboardText && isValidUrl(clipboardText)) {
setValue("url", clipboardText);
return;
}
Expand All @@ -199,7 +206,7 @@ export default function DownloadVideo() {
if (preferences.autoLoadUrlFromSelectedText) {
try {
const selectedText = await getSelectedText();
if (selectedText && isYouTubeURL(selectedText)) {
if (selectedText && isValidUrl(selectedText)) {
setValue("url", selectedText);
return;
}
Expand All @@ -211,7 +218,7 @@ export default function DownloadVideo() {
if (preferences.enableBrowserExtensionSupport) {
try {
const tabUrl = (await BrowserExtension.getTabs()).find((tab) => tab.active)?.url;
if (tabUrl && isYouTubeURL(tabUrl)) setValue("url", tabUrl);
if (tabUrl && isValidUrl(tabUrl)) setValue("url", tabUrl);
} catch {
// Suppress the error if Raycast didn't find browser extension
}
Expand Down Expand Up @@ -245,11 +252,18 @@ export default function DownloadVideo() {
icon={Icon.Download}
title="Download Video"
onSubmit={(values) => {
setWarning("");
handleSubmit({ ...values, copyToClipboard: false } as DownloadOptions);
}}
/>
</ActionPanel>
}
searchBarAccessory={
<Form.LinkAccessory
text="Supported Sites"
target="https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md"
/>
}
>
<Form.Description title="Title" text={video?.title ?? "Video not found"} />
<Form.TextField
Expand All @@ -258,6 +272,7 @@ export default function DownloadVideo() {
placeholder="https://www.youtube.com/watch?v=xRMPKQweySE"
{...itemProps.url}
/>
{warning && <Form.Description text={warning} />}
{/*<Form.Separator />*/}
{/*<Form.TextField*/}
{/* info="Optional. Specify when the output video should start. Follow the format HH:MM:SS or MM:SS."*/}
Expand Down
25 changes: 3 additions & 22 deletions extensions/youtube-downloader/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getPreferenceValues } from "@raycast/api";
import { formatDuration, intervalToDuration } from "date-fns";
import isUrlSuperb from "is-url-superb";

export const preferences = getPreferenceValues<ExtensionPreferences>();

Expand Down Expand Up @@ -47,26 +48,6 @@ export function isValidHHMM(input: string) {
}
}

export function isYouTubeURL(input: string) {
const validHostnames = new Set(["youtube.com", "www.youtube.com", "youtu.be"]);
const videoIdPattern = /^[a-zA-Z0-9_-]{11}$/;
const linkProtocolPrefix = "https://";
if (!input.startsWith(linkProtocolPrefix)) input = linkProtocolPrefix + input;
try {
const url = new URL(input);
if (!validHostnames.has(url.hostname)) return false;

if (url.hostname === "youtu.be") {
return videoIdPattern.test(url.pathname.slice(1));
}

if (url.pathname.startsWith("/embed/")) {
return videoIdPattern.test(url.pathname.slice(7));
}

const videoId = url.searchParams.get("v");
return videoId ? videoIdPattern.test(videoId) : false;
} catch {
return false;
}
export function isValidUrl(url: string) {
return isUrlSuperb(url, { lenient: true });
}
Loading