Skip to main content

youtube: replace /v/ URLs with /watch?v= URLs

ID
c68d4a9
date
2026-08-06 06:10:33+00:00
author
Alex Chan <alex@alexwlchan.net>
parent
edd1044
message
youtube: replace /v/ URLs with /watch?v= URLs
changed files
2 files, 24 additions, 6 deletions

Changed files

test_yt-dlp_alexwlchan.py (4222 → 4891)

diff --git a/test_yt-dlp_alexwlchan.py b/test_yt-dlp_alexwlchan.py
index 643151f..90c7a8c 100644
--- a/test_yt-dlp_alexwlchan.py
+++ b/test_yt-dlp_alexwlchan.py
@@ -104,6 +104,7 @@ def test_instagram_video() -> None:
 @pytest.mark.parametrize(
     "url, expected",
     [
+        # Instagram and YouTube URLs in the correct format are left as-is.
         (
             "https://www.instagram.com/reel/DMWY8KkOS0n/",
             "https://www.instagram.com/reel/DMWY8KkOS0n/",
@@ -116,14 +117,29 @@ def test_instagram_video() -> None:
             "https://www.youtube.com/watch?v=0N1_0SUGlDQ",
             "https://www.youtube.com/watch?v=0N1_0SUGlDQ",
         ),
+        # Extra query parameters are stripped from YouTube URLs
         (
             "https://www.youtube.com/watch?v=0N1_0SUGlDQ&app=desktop&list=LL&index=43",
             "https://www.youtube.com/watch?v=0N1_0SUGlDQ",
         ),
+        # YouTube /embed/ and /v/ links are replaced with /watch?v=
         (
             "https://www.youtube.com/embed/0N1_0SUGlDQ",
             "https://www.youtube.com/watch?v=0N1_0SUGlDQ",
         ),
+        (
+            "https://www.youtube.com/v/uoLoyg3JKRQ",
+            "https://www.youtube.com/watch?v=uoLoyg3JKRQ",
+        ),
+        # YouTube URLs are upgraded from HTTP to HTTPS
+        (
+            "http://www.youtube.com/embed/0N1_0SUGlDQ",
+            "https://www.youtube.com/watch?v=0N1_0SUGlDQ",
+        ),
+        (
+            "http://www.youtube.com/watch?v=0N1_0SUGlDQ",
+            "https://www.youtube.com/watch?v=0N1_0SUGlDQ",
+        ),
     ],
 )
 def test_normalise_url(url: str, expected: str) -> None:

yt-dlp_alexwlchan.py (9463 → 9588)

diff --git a/yt-dlp_alexwlchan.py b/yt-dlp_alexwlchan.py
index 31d74a4..3f7d896 100755
--- a/yt-dlp_alexwlchan.py
+++ b/yt-dlp_alexwlchan.py
@@ -15,12 +15,11 @@ from typing import Any, TypedDict
 import urllib.parse
 
 from chives.fetch import download_image
-from chives.media import create_video_entity, VideoEntity
+from chives.media import VideoEntity, create_video_entity
 from yt_dlp import YoutubeDL
 from yt_dlp.networking.exceptions import HTTPError as YouTubeDLHTTPError
 from yt_dlp.utils import DownloadError
 
-
 ydl_opts: Any = {
     # Print progress output to stderr, not stdout
     "logtostderr": True,
@@ -59,23 +58,26 @@ def normalise_url(url: str) -> str:
     """
     u = urllib.parse.urlsplit(url)
 
-    # If it's a YouTube embed URL, change it to the watch?v= form
+    # If it's a YouTube /embed/ or /v/ URL, change it to the watch?v= form
+    #
+    # The former is quite common in embeds; I've seen the /v/ form used
+    # in posts on leancrew.com.
     if (
         u.netloc == "www.youtube.com"
-        and u.path.startswith("/embed/")
+        and u.path.startswith(("/embed/", "/v/"))
         and len(u.path.split("/")) == 3
     ):
         video_id = u.path.split("/")[-1]
         qs = [("v", video_id)]
         query = urllib.parse.urlencode(qs)
-        return urllib.parse.urlunsplit((u.scheme, u.netloc, "/watch", query, ""))
+        return urllib.parse.urlunsplit(("https", u.netloc, "/watch", query, ""))
 
     # If it's a YouTube URL, remove all query parameters except video ID (v)
     if u.netloc == "www.youtube.com":
         qs = urllib.parse.parse_qsl(u.query)
         qs = [(k, v) for k, v in qs if k == "v"]
         query = urllib.parse.urlencode(qs)
-        return urllib.parse.urlunsplit((u.scheme, u.netloc, u.path, query, ""))
+        return urllib.parse.urlunsplit(("https", u.netloc, u.path, query, ""))
 
     return url