Add a script for listing YouTube videos I’ve liked
- ID
8852e18- date
2024-02-15 08:10:08+00:00- author
Alex Chan <alex@alexwlchan.net>- parent
56686e1- message
Add a script for listing YouTube videos I've liked- changed files
2 files, 160 additions, 1 deletion
Changed files
web/README.md (4353) → web/README.md (4832)
diff --git a/web/README.md b/web/README.md
index 491c4a6..d05aede 100644
--- a/web/README.md
+++ b/web/README.md
@@ -30,6 +30,12 @@ scripts = [
"""
},
{
+ "usage": 'list_liked_youtube_videos.py > liked_videos.$(date +"%Y-%m-%d").txt',
+ "description": """
+ print the URL of every video I've liked on YouTube.
+ """
+ },
+ {
"usage": "rcurl [URL]",
"description": """
call curl with a couple of flags that allow it to do resumable downloads, which is useful for large files.
@@ -90,6 +96,15 @@ cog_helpers.create_description_table(folder_name=folder_name, scripts=scripts)
</dd>
<dt>
+ <a href="https://github.com/alexwlchan/scripts/blob/main/web/list_liked_youtube_videos.py">
+ <code>list_liked_youtube_videos.py > liked_videos.$(date +"%Y-%m-%d").txt</code>
+ </a>
+ </dt>
+ <dd>
+ print the URL of every video I've liked on YouTube.
+ </dd>
+
+ <dt>
<a href="https://github.com/alexwlchan/scripts/blob/main/web/rcurl">
<code>rcurl [URL]</code>
</a>
@@ -143,4 +158,4 @@ cog_helpers.create_description_table(folder_name=folder_name, scripts=scripts)
this is a wrapper around <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> that does parallel downloads of videos in playlists.
</dd>
</dl>
-<!-- [[[end]]] (checksum: a4f4aaedc92d2ce7e499f50a87c39d22) -->
+<!-- [[[end]]] (checksum: 1bdadef9eb851d3d98d6e7d78e37aa11) -->
web/list_liked_youtube_videos.py (0) → web/list_liked_youtube_videos.py (5104)
diff --git a/web/list_liked_youtube_videos.py b/web/list_liked_youtube_videos.py
new file mode 100755
index 0000000..c7d32d2
--- /dev/null
+++ b/web/list_liked_youtube_videos.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python3
+"""
+Get a list of my Liked videos on YouTube.
+
+It prints the URL of each video, newest first.
+
+Example use:
+
+ $ python3 web/list_liked_youtube_videos.py > liked_videos.$(date +"%Y-%m-%d").txt
+
+"""
+
+import contextlib
+import datetime
+import json
+import sys
+
+import google.oauth2.credentials
+import googleapiclient.discovery # pip install google-api-python-client==1.7.2
+import google_auth_oauthlib.flow # pip install google-auth-oauthlib==0.4.1
+import keyring
+
+
+class YouTubeClient:
+ def __init__(self, label: str):
+ self.youtube = self.create_youtube_client(label)
+
+ def create_youtube_client(self, label: str):
+ """
+ Get an authenticated OAuth client for YouTube.
+
+ It gets the OAuth config from the system keychain, and caches
+ per-user credentials in the keychain under ("youtube", label).
+ """
+ api_service_name = "youtube"
+ api_version = "v3"
+ scopes = ["https://www.googleapis.com/auth/youtube.readonly"]
+
+ # Try to retrieve a stored OAuth access token from the keychain.
+ #
+ # This saves me going through the in-browser authentication flow
+ # if I've already run the script.
+ stored_credentials = keyring.get_password("youtube", label)
+
+ if stored_credentials is not None:
+ json_credentials = json.loads(stored_credentials)
+
+ if "expiry" in json_credentials:
+ expiry = datetime.datetime.fromisoformat(json_credentials["expiry"])
+ expiry = expiry.replace(tzinfo=None)
+ json_credentials["expiry"] = expiry
+
+ credentials = google.oauth2.credentials.Credentials(**json_credentials)
+
+ # If there are no stored credentials, fetch new ones.
+ else:
+ # Retrieve the OAuth client credentials from the keychain.
+ #
+ # This contains the contents of the JSON file that I downloaded
+ # from the Google Cloud console, but now those credentials aren't
+ # just saved as a plaintext file on disk.
+ stored_client_secrets = keyring.get_password("youtube", "client_secrets")
+ if stored_client_secrets is None:
+ raise ValueError("Could not find OAuth client secrets in keychain!")
+
+ flow = google_auth_oauthlib.flow.InstalledAppFlow.from_client_config(
+ client_config=json.loads(stored_client_secrets), scopes=scopes
+ )
+
+ with contextlib.redirect_stdout(sys.stderr):
+ credentials = flow.run_local_server()
+
+ # Save these credentials in the system keychain, so they can be
+ # retrieved later.
+ keyring.set_password("youtube", label, credentials.to_json())
+
+ youtube = googleapiclient.discovery.build(
+ api_service_name, api_version, credentials=credentials
+ )
+
+ # The OAuth credentials don't last forever -- they seem to expire after
+ # a week. This is a slightly ropey attempt to work around that.
+ #
+ # If we call the API and the saved token is expired, just delete
+ # it and get new creds -- sending me back through the in-browser flow.
+ #
+ # Notes:
+ #
+ # - There are ways to refresh OAuth tokens that don't involve
+ # sending me back through the in-browser flow, but I didn't
+ # look at them as part of this project.
+ # - Catching all exceptions is a bit broad. This code should really
+ # retry only if it gets a "credentials expired" exception, and
+ # throw any other exceptions immediately.
+ #
+ try:
+ request = youtube.channels().list(part="snippet", mine=True)
+ request.execute()
+ except Exception as e:
+ print(e)
+ keyring.delete_password("youtube", label)
+ return self.create_youtube_client(label)
+ else:
+ return youtube
+
+ def get_liked_videos(self):
+ """
+ Generate a list of videos that this YouTube account has liked.
+ """
+ kwargs = {"part": "snippet", "playlistId": "LL", "maxResults": "50"}
+
+ while True:
+ request = self.youtube.playlistItems().list(**kwargs)
+ response = request.execute()
+
+ yield from response["items"]
+
+ try:
+ kwargs["pageToken"] = response["nextPageToken"]
+ except KeyError:
+ break
+
+
+if __name__ == "__main__":
+ youtube = YouTubeClient(label="download_liked_videos")
+
+ # request = youtube.youtube.playlistItems().list(
+ # part="snippet,contentDetails",
+ # # onBehalfOfContentOwner=True,
+ # playlistId='LL'
+ # )
+ # response = request.execute()
+ #
+ # # liked_playlist_id = next(
+ # # playlist['id']
+ # # for playlist in response['items']
+ # # if
+ # # )
+ #
+ # from pprint import pprint; pprint(response)
+
+ for video in youtube.get_liked_videos():
+ video_id = video["id"]
+ print(f"https://www.youtube.com/watch?v={video_id}")