Fork me on GitHub

Project Notes

#405 YouTube Time-stamped Link Maker

I wanted an easy way to generate markdown links to specific times in YouTube videos, using the actual video title and rendering the time in a more convenient hh:mm:ss format.

Notes

When making notes for courses, or music study for example, I often want to create markdown links to specific timestamped sections in videos.

Say, for example, I have a link from YouTube like this:

https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240

I’d like to be able to generate a markdown link in a few different formats.

Firstly, a simple markdown link using the actual video title, and the timestamp in “hh:mm:ss” instead of total seconds, i.e.:

[LIV MOON - THE WINTER (VIVALDI FOUR SEASONS) (00:04:00)](https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240)

Or sometimes, substituting my own title text e.g.

[Custom Title Text (00:04:00)](https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240)

Or finally, as a video image link e.g.

[![LIV MOON - THE WINTER (VIVALDI FOUR SEASONS) (00:04:00)](https://img.youtube.com/vi/ky2z2mMQO40/0.jpg)](https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240)

Using the script yt-md-link.py in the various ways…

Invocation:

$ ./yt-md-link.py "https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240"
[LIV MOON - THE WINTER (VIVALDI FOUR SEASONS) (00:04:00)](https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240)

The markdown renders as follows:

LIV MOON - THE WINTER (VIVALDI FOUR SEASONS) (00:04:00)

Note: if there’s no timestamp in the video link, it just renders the plain link:

$ ./yt-md-link.py "https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv"
[LIV MOON - THE WINTER (VIVALDI FOUR SEASONS)](https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv)

The markdown renders as follows:

LIV MOON - THE WINTER (VIVALDI FOUR SEASONS)

Invocation:

$ ./yt-md-link.py "https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240" -t "Custom Title"
[Custom Title (00:04:00)](https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240)

The markdown renders as follows:

Custom Title (00:04:00)

Invocation:

$ ./yt-md-link.py "https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240" -i
[![LIV MOON - THE WINTER (VIVALDI FOUR SEASONS) (00:04:00)](https://img.youtube.com/vi/ky2z2mMQO40/0.jpg)](https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240)

The markdown renders as follows:

LIV MOON - THE WINTER (VIVALDI FOUR SEASONS) (00:04:00)

Invocation:

$ ./yt-md-link.py "https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240" -i -t "Custom Title"
[![Custom Title (00:04:00)](https://img.youtube.com/vi/ky2z2mMQO40/0.jpg)](https://youtu.be/ky2z2mMQO40?si=ek-z2G0Rpv0Uz4fv&t=240)

The markdown renders as follows:

Custom Title (00:04:00)

The Script

The final version of the script yt-md-link.py is listed below.

#!/usr/bin/env python3
"""
Turns a YouTube URL with timestamp into a Markdown link
"""

import argparse
import sys
import re
import json
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
import urllib.parse

def parse_timestamp(url: str) -> str:
    parsed = urllib.parse.urlparse(url)
    qs = urllib.parse.parse_qs(parsed.query)
    t_str = qs.get("t", [None])[0] or re.search(r"[?&]t=(\d+)s?", url)
    if t_str is None:
        return ""

    if isinstance(t_str, str):
        # came from query string
        seconds = int(t_str)
    else:
        # came from regex
        seconds = int(t_str.group(1))

    hh, mm = divmod(seconds, 3600)
    mm, ss = divmod(mm, 60)
    marker = f" ({hh:02d}:{mm:02d}:{ss:02d})"
    return marker

def get_title(url: str) -> str:
    params = urllib.parse.urlencode({"url": url, "format": "json"})
    oembed_url = f"https://www.youtube.com/oembed?{params}"
    req = Request(oembed_url, headers={"User-Agent": "Mozilla/5.0"})
    with urlopen(req, timeout=10) as resp:
        data = resp.read().decode("utf-8")
    info = json.loads(data)
    title = info.get("title")
    if not title:
        raise RuntimeError("oEmbed response missing title")
    return title

def get_video_id(url: str) -> str:
      video_id_match = re.search(r"(?:v=|youtu\.be/)([a-zA-Z0-9_-]{11})", url)
      if not video_id_match:
        sys.exit("Error: Could not extract video ID from URL")
      video_id = video_id_match.group(1)
      return video_id

def main() -> None:
    parser = argparse.ArgumentParser(prog="yt-md-link.py", description="Turn a YouTube URL with timestamp into a Markdown link or image.")
    parser.add_argument("url", help="YouTube URL with timestamp")
    parser.add_argument("-t", "--title", help="Override fetched title", metavar="STRING")
    parser.add_argument("-i", "--image", action="store_true", help="Output image markdown instead of a link")
    args = parser.parse_args()

    video_id = get_video_id(args.url)
    time_marker = parse_timestamp(args.url)
    title = args.title if args.title is not None else get_title(args.url)

    if args.image:
      print(f"[![{title}{time_marker}](https://img.youtube.com/vi/{video_id}/0.jpg)]({args.url})")
    else:
      print(f"[{title}{time_marker}]({args.url})")

if __name__ == "__main__":
    main()

Credits and References

About LCK#405
python

This page is a web-friendly rendering of my project notes shared in the LittleCodingKata GitHub repository.

Project Source on GitHub Return to the LittleCodingKata Catalog
About LittleCodingKata

LittleCodingKata is my collection of programming exercises, research and code toys broadly spanning things that relate to programming and software development (languages, frameworks and tools).

These range from the trivial to the complex and serious. Many are inspired by existing work and I'll note credits and references where applicable. The focus is quite scattered, as I variously work on things new and important in the moment, or go back to revisit things from the past.

This is primarily a personal collection for my own edification and learning, but anyone who stumbles by is welcome to borrow, steal or reference the work here. And if you spot errors or issues I'd really appreciate some feedback - create an issue, send me an email or even send a pull-request.

Follow the Blog follow projects and notes as they are published in your favourite feed reader