Skip to main content

Merge pull request #12 from alexwlchan/mypy

ID
091a9a5
date
2025-10-19 06:20:56+00:00
author
Alex Chan <alex@alexwlchan.net>
parents
4b36185, 9246d0f
message
Merge pull request #12 from alexwlchan/mypy

all: add type checking with Mypy
changed files
6 files, 45 additions, 28 deletions

Changed files

.github/workflows/test.yml (897) → .github/workflows/test.yml (952)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1e179b3..fdf2bb9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -36,6 +36,9 @@ jobs:
         ruff check .
         ruff format --check .
 
+    - name: Check types
+      run: mypy *.py --strict
+
     # I don't run tests in GitHub Actions because YouTube blocks yt-dlp
     # without authentication, and I don't want to sort out passing cookies
     # in CI for now -- I can run the tests locally, that's good enough.

dev_requirements.in (33) → dev_requirements.in (51)

diff --git a/dev_requirements.in b/dev_requirements.in
index 368464a..6737fd3 100644
--- a/dev_requirements.in
+++ b/dev_requirements.in
@@ -1,4 +1,6 @@
 -r requirements.txt
 
+mypy
 pytest
 ruff
+types-yt-dlp

dev_requirements.txt (1016) → dev_requirements.txt (1270)

diff --git a/dev_requirements.txt b/dev_requirements.txt
index b22ca4c..8a8d1cf 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -6,22 +6,28 @@ certifi==2025.10.5
     # via
     #   -r requirements.txt
     #   requests
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
     # via
     #   -r requirements.txt
     #   requests
-gallery-dl==1.30.9
+gallery-dl==1.30.10
     # via -r requirements.txt
-idna==3.10
+idna==3.11
     # via
     #   -r requirements.txt
     #   requests
-iniconfig==2.1.0
+iniconfig==2.3.0
     # via pytest
 mutagen==1.47.0
     # via -r requirements.txt
+mypy==1.18.2
+    # via -r dev_requirements.in
+mypy-extensions==1.1.0
+    # via mypy
 packaging==25.0
     # via pytest
+pathspec==0.12.1
+    # via mypy
 pluggy==1.6.0
     # via pytest
 pycryptodomex==3.23.0
@@ -34,13 +40,19 @@ requests==2.32.5
     # via
     #   -r requirements.txt
     #   gallery-dl
-ruff==0.13.3
+ruff==0.14.1
+    # via -r dev_requirements.in
+types-yt-dlp==2025.9.26.20251009
     # via -r dev_requirements.in
+typing-extensions==4.15.0
+    # via mypy
 urllib3==2.5.0
     # via
     #   -r requirements.txt
     #   requests
 websockets==15.0.1
-    # via -r requirements.txt
-yt-dlp==2025.9.26
+    # via
+    #   -r requirements.txt
+    #   types-yt-dlp
+yt-dlp==2025.10.14
     # via -r requirements.txt

requirements.txt (618) → requirements.txt (620)

diff --git a/requirements.txt b/requirements.txt
index 946486c..5f4e911 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,11 +6,11 @@ certifi==2025.10.5
     # via
     #   requests
     #   yt-dlp
-charset-normalizer==3.4.3
+charset-normalizer==3.4.4
     # via requests
-gallery-dl==1.30.9
+gallery-dl==1.30.10
     # via -r requirements.in
-idna==3.10
+idna==3.11
     # via requests
 mutagen==1.47.0
     # via yt-dlp
@@ -26,5 +26,5 @@ urllib3==2.5.0
     #   yt-dlp
 websockets==15.0.1
     # via yt-dlp
-yt-dlp==2025.9.26
+yt-dlp==2025.10.14
     # via -r requirements.in

tests/test_yt-dlp_alexwlchan.py (1505) → test_yt-dlp_alexwlchan.py (1508)

diff --git a/tests/test_yt-dlp_alexwlchan.py b/test_yt-dlp_alexwlchan.py
similarity index 82%
rename from tests/test_yt-dlp_alexwlchan.py
rename to test_yt-dlp_alexwlchan.py
index 9e5c234..5b28cf2 100644
--- a/tests/test_yt-dlp_alexwlchan.py
+++ b/test_yt-dlp_alexwlchan.py
@@ -1,9 +1,10 @@
 import json
 import os
 import subprocess
+from typing import Any
 
 
-def download_video(url):
+def download_video(url: str) -> Any:
     output = subprocess.check_output(["python3", "yt-dlp_alexwlchan.py", url])
     video_info = json.loads(output)
 
@@ -25,7 +26,7 @@ def test_youtube_video() -> None:
     assert video_info["subtitle_path"] is None
 
     assert video_info["id"] == "TUQaGhPdlxs"
-    assert video_info["date_uploaded"] == "2008-04-19T03:51:21Z"
+    assert video_info["date_uploaded"] == "2022-03-25T01:10:38Z"
 
 
 def test_instagram_video() -> None:
@@ -40,10 +41,7 @@ def test_instagram_video() -> None:
 
     assert video_info["channel"]["id"] == "52716733233"
     assert video_info["channel"]["name"] == "Public Domain Gems"
-    assert (
-        video_info["channel"]["channel_url"]
-        == "https://www.instagram.com/publicdomaingems/"
-    )
+    assert video_info["channel"]["url"] == "https://www.instagram.com/publicdomaingems/"
 
     assert video_info["id"] == "DMWY8KkOS0n"
-    assert video_info["date_uploaded"] == "2008-04-19T03:51:21Z"
+    assert video_info["date_uploaded"] == "2025-07-21T00:34:41Z"

yt-dlp_alexwlchan.py (4890) → yt-dlp_alexwlchan.py (4959)

diff --git a/yt-dlp_alexwlchan.py b/yt-dlp_alexwlchan.py
index b7b485b..ce6f547 100755
--- a/yt-dlp_alexwlchan.py
+++ b/yt-dlp_alexwlchan.py
@@ -6,12 +6,12 @@ from pathlib import Path
 import subprocess
 import sys
 import tempfile
-from typing import TypedDict
+from typing import Any, TypedDict
 
 from yt_dlp import YoutubeDL
 
 
-ydl_opts = {
+ydl_opts: Any = {
     # Print progress output to stderr, not stdout
     "logtostderr": True,
     #
@@ -44,7 +44,7 @@ def get_youtube_avatar_url(channel_url: str) -> str:
     """
     Returns the avatar URL of a YouTube channel.
     """
-    ydl_opts = {
+    ydl_opts: Any = {
         # Print progress output to stderr, not stdout
         "logtostderr": True,
         #
@@ -57,11 +57,11 @@ def get_youtube_avatar_url(channel_url: str) -> str:
     }
 
     with YoutubeDL(ydl_opts) as ydl:
-        channel_info = ydl.extract_info(channel_url, download=False)
+        channel_info: Any = ydl.extract_info(channel_url, download=False)
 
     thumbnails = channel_info["thumbnails"]
     best_thumbnail = next(t for t in thumbnails if t["id"] == "avatar_uncropped")
-    return best_thumbnail["url"]
+    return str(best_thumbnail["url"])
 
 
 def get_instagram_avatar_url(channel_name: str) -> str:
@@ -88,10 +88,10 @@ class VideoInfo(TypedDict):
     url: str
     title: str
     description: str
-    date_posted: str
+    date_uploaded: str
     video_path: Path
     thumbnail_path: Path
-    subtitle_path: Path
+    subtitle_path: Path | None
     channel: ChannelInfo
     site: str
 
@@ -103,7 +103,7 @@ def download_video(url: str) -> VideoInfo:
     ydl_opts["outtmpl"] = str(tmp_dir / "%(title)s.%(ext)s")
 
     with YoutubeDL(ydl_opts) as ydl:
-        video_info = ydl.extract_info(url)
+        video_info: Any = ydl.extract_info(url)
 
     video_path = next(p for p in tmp_dir.iterdir() if p.suffix == ".mp4")
     thumbnail_path = next(p for p in tmp_dir.iterdir() if p.suffix == ".jpg")
@@ -112,6 +112,8 @@ def download_video(url: str) -> VideoInfo:
     except StopIteration:
         subtitle_path = None
 
+    channel: ChannelInfo
+
     if video_info["extractor"] == "youtube":
         site = "youtube"
         channel = {
@@ -127,7 +129,7 @@ def download_video(url: str) -> VideoInfo:
         channel = {
             "id": video_info["uploader_id"],
             "name": video_info["uploader"],
-            "channel_url": f"https://www.instagram.com/{video_info['channel']}/",
+            "url": f"https://www.instagram.com/{video_info['channel']}/",
             "avatar_url": get_instagram_avatar_url(channel_name=video_info["channel"]),
         }
     else:
@@ -154,7 +156,7 @@ class PathEncoder(json.JSONEncoder):
     Custom JSON encoder that encodes paths as a string.
     """
 
-    def default(self, o):
+    def default(self, o: Any) -> Any:
         if isinstance(o, Path):
             return str(o.absolute())
         else: