Skip to content

Commit e1ee393

Browse files
committed
ytm: Obtain credentials from web browser
1 parent dbab2b2 commit e1ee393

File tree

4 files changed

+166
-61
lines changed

4 files changed

+166
-61
lines changed

bin/sync-music-library

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,6 @@
77

88
set -eo pipefail
99

10-
__ytm_with_user_credentials() {
11-
ytm "${YTM_OAUTH_CLIENT_ID}" <(pass "${YTM_CREDENTIALS_PASS_PATH}") "$@"
12-
}
13-
14-
__ytm() {
15-
if [ -z "${YTM_OAUTH_CLIENT_SECRET_PASS_PATH}" ]; then
16-
__ytm_with_user_credentials "$@"
17-
return
18-
fi
19-
20-
pass "${YTM_OAUTH_CLIENT_SECRET_PASS_PATH}" |
21-
__ytm_with_user_credentials "$@"
22-
}
23-
2410
commit_changes() {
2511
if git diff --quiet "${MUSIC_LIBRARY_DATABASE_PATH}"; then
2612
echo "Library database is up-to-date."
@@ -36,28 +22,13 @@ commit_changes() {
3622
}
3723

3824
main() {
39-
if [ -z "${YTM_CREDENTIALS_PASS_PATH}" ]; then
40-
echo "\$YTM_CREDENTIALS_PASS_PATH: Variable unset" >&2
41-
exit 1
42-
fi
43-
44-
if [ -z "${YTM_OAUTH_CLIENT_ID}" ]; then
45-
echo "\$YTM_OAUTH_CLIENT_ID: Variable unset" >&2
46-
exit 1
47-
fi
48-
49-
if [ -z "${YTM_OAUTH_CLIENT_SECRET_PASS_PATH}" ]; then
50-
echo "\$YTM_OAUTH_CLIENT_SECRET_PASS_PATH: Variable unset" >&2
51-
exit 1
52-
fi
53-
5425
if [ -f "${MUSIC_LIBRARY_DATABASE_PATH}" ]; then
5526
echo "Exporting library…"
5627

5728
cd "$(dirname "${MUSIC_LIBRARY_DATABASE_PATH}")"
5829

5930
git checkout -fq master
60-
__ytm export-library >"${MUSIC_LIBRARY_DATABASE_PATH}"
31+
ytm export-library >"${MUSIC_LIBRARY_DATABASE_PATH}"
6132
commit_changes
6233

6334
cd - >/dev/null
@@ -70,13 +41,13 @@ main() {
7041
if [ -n "${MUSIC_LIBRARY_TARGET_PLAYLIST}" ]; then
7142
echo "Syncing '${MUSIC_LIBRARY_TARGET_PLAYLIST}' with library…"
7243

73-
__ytm sync-playlist "${MUSIC_LIBRARY_TARGET_PLAYLIST}"
44+
ytm sync-playlist "${MUSIC_LIBRARY_TARGET_PLAYLIST}"
7445
else
7546
echo "\$MUSIC_LIBRARY_TARGET_PLAYLIST: Variable unset" >&2
7647
fi
7748

78-
echo "Downloading library…"
79-
__ytm download-library
49+
# echo "Downloading library…"
50+
# ytm download-library
8051
}
8152

8253
main "$@"

ytm/poetry.lock

Lines changed: 118 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ytm/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ description = ""
88
authors = []
99

1010
[tool.poetry.dependencies]
11+
# TODO: Install from PyPI once no-binary dist. is available.
12+
playwright = {git = "https://github.com/microsoft/playwright-python.git", rev = "v1.58.0"}
1113
python = "^3.12"
1214
yt-dlp = ">=2025.1.26" # https://github.com/ytdl-org/youtube-dl/issues/29494
1315
ytmusicapi = "^1.10.3"

ytm/ytm/__main__.py

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33

44
import argparse
55
import csv
6-
from getpass import getpass
7-
import json
86
import operator
97
from pathlib import Path
8+
import shutil
109
import sys
10+
from urllib.parse import urlsplit
1111

12+
from playwright.sync_api import sync_playwright
1213
from yt_dlp import YoutubeDL
14+
import ytmusicapi
1315
from ytmusicapi import YTMusic
14-
from ytmusicapi.auth.oauth import OAuthCredentials
1516

1617
GET_LIMIT = None
1718

19+
PLATWRIGHT_TIMEOUT = 5 * 60 * 1000
20+
1821

1922
def get_song_entry(song):
2023
return {
@@ -154,15 +157,6 @@ def sync_playlist(client, playlist_id):
154157

155158
def main():
156159
arg_parser = argparse.ArgumentParser()
157-
arg_parser.add_argument(
158-
"oauth_client_id",
159-
help="OAuth Client ID",
160-
)
161-
arg_parser.add_argument(
162-
"user_credentials",
163-
help="File containing user OAuth credentials",
164-
type=argparse.FileType(),
165-
)
166160
subparsers = arg_parser.add_subparsers(dest="command", required=True)
167161
subparsers.add_parser(
168162
"download-library",
@@ -183,25 +177,46 @@ def main():
183177

184178
args = arg_parser.parse_args()
185179

186-
with args.user_credentials:
187-
user_credentials = json.loads(args.user_credentials.read())
188-
189-
try:
190-
oauth_client_secret = (
191-
getpass("OAuth Client Secret: ") if sys.stdin.isatty() else input()
180+
with sync_playwright() as p:
181+
browser = p.chromium.launch_persistent_context(
182+
user_data_dir=(
183+
Path.home() / ".config/ytm-google-chrome-authenticated"
184+
),
185+
executable_path=shutil.which("google-chrome-stable"),
186+
args=[
187+
# Required for Google authentication.
188+
"--disable-blink-features=AutomationControlled",
189+
],
190+
# Required to prevent bot detection and to perform manual actions
191+
# when necessary.
192+
headless=False,
192193
)
193-
except EOFError:
194-
print( # noqa: T201
195-
"OAuth Client Secret could not be read.",
196-
file=sys.stderr,
194+
page = browser.new_page()
195+
196+
with page.expect_response(
197+
lambda response: (
198+
response.request.method == "POST"
199+
and urlsplit(response.url)[:3]
200+
== ("https", "music.youtube.com", "/youtubei/v1/browse")
201+
),
202+
timeout=PLATWRIGHT_TIMEOUT,
203+
) as response_info:
204+
page.goto(
205+
"https://music.youtube.com/library",
206+
timeout=PLATWRIGHT_TIMEOUT,
207+
)
208+
209+
headers = "\n".join(
210+
f"{header['name']}: {header['value']}"
211+
for header in response_info.value.request.headers_array()
197212
)
198-
return 1
213+
214+
page.close()
215+
browser.close()
199216

200217
client = YTMusic(
201-
user_credentials,
202-
oauth_credentials=OAuthCredentials(
203-
args.oauth_client_id,
204-
oauth_client_secret,
218+
ytmusicapi.setup(
219+
headers_raw=headers,
205220
),
206221
)
207222

0 commit comments

Comments
 (0)