Rework/fix authorization

Co-Authored-By: Arsenii es3n1n <40367813+es3n1n@users.noreply.github.com>
This commit is contained in:
Rafael Moraes
2025-03-19 17:01:44 -03:00
co-authored by Arsenii es3n1n
parent 96477e276a
commit 4de9fc97c2
3 changed files with 59 additions and 30 deletions
+5 -8
View File
@@ -401,7 +401,7 @@ def main(
return
logger.info("Starting Votify")
spotify_api = SpotifyApi.from_cookies_file(cookies_path)
if spotify_api.config_info["isAnonymous"]:
if spotify_api.session_info["isAnonymous"]:
logger.critical(
"Failed to get a valid session. Try logging in and exporting your cookies again"
)
@@ -459,8 +459,8 @@ def main(
downloader_music_video = DownloaderMusicVideo(
downloader_video,
)
spotify_api.config_info["isPremium"] = (
True if force_premium else spotify_api.config_info["isPremium"]
is_premium = (
True if force_premium else spotify_api.user_profile["product"] == "premium"
)
if not lrc_only:
if audio_quality in AAC_AUDIO_QUALITIES:
@@ -497,10 +497,7 @@ def main(
if download_mode == DownloadMode.ARIA2C and not downloader.aria2c_path_full:
logger.critical(X_NOT_FOUND_STRING.format("aria2c", aria2c_path))
return
if (
not spotify_api.config_info["isPremium"]
and audio_quality in PREMIUM_AUDIO_QUALITIES
):
if not is_premium and audio_quality in PREMIUM_AUDIO_QUALITIES:
logger.critical("Cannot download at chosen quality with a free account")
return
can_download_music_videos = True
@@ -532,7 +529,7 @@ def main(
)
else:
downloader.set_cdm()
if not spotify_api.config_info["isPremium"]:
if is_premium:
music_video_warning_message.append(
"Cannot download music videos with a non-premium account"
)
+25 -22
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
import functools
import json
import re
import time
import typing
from http.cookiejar import CookieJar, MozillaCookieJar
@@ -11,6 +10,7 @@ from pathlib import Path
import base62
import requests
from .totp import TOTP
from .utils import check_response
@@ -50,6 +50,7 @@ class SpotifyApi:
return cls(cookies)
def _set_session(self):
self.totp = TOTP()
self.session = requests.Session()
if self.cookies is not None:
self.session.cookies.update(self.cookies)
@@ -72,25 +73,29 @@ class SpotifyApi:
"app-platform": "WebPlayer",
}
)
self._set_session_auth()
self._set_session_info()
self._set_user_profile()
def _set_session_auth(self):
home_page = self.get_home_page()
self.session_info = json.loads(
re.search(
r'<script id="session" data-testid="session" type="application/json">(.+?)</script>',
home_page,
).group(1)
)
self.config_info = json.loads(
re.search(
r'<script id="config" data-testid="config" type="application/json">(.+?)</script>',
home_page,
).group(1)
def _set_session_info(self):
server_time_response = self.session.get("https://open.spotify.com/server-time")
check_response(server_time_response)
server_time = 1e3 * server_time_response.json()["serverTime"]
totp = self.totp.generate(timestamp=server_time)
session_info_response = self.session.get(
"https://open.spotify.com/get_access_token",
params={
"reason": "init",
"productType": "web-player",
"totp": totp,
"totpVer": str(self.totp.version),
"ts": str(server_time),
},
)
check_response(session_info_response)
self.session_info = session_info_response.json()
self.session.headers.update(
{
"Authorization": f"Bearer {self.session_info['accessToken']}",
"authorization": f"Bearer {self.session_info['accessToken']}",
}
)
@@ -101,14 +106,12 @@ class SpotifyApi:
timestamp_now = time.time() * 1000
if timestamp_now < timestamp_session_expire:
return
self._set_session_auth()
self._set_session_info()
def get_home_page(self) -> str:
response = self.session.get(
SpotifyApi.SPOTIFY_HOME_PAGE_URL,
)
def _set_user_profile(self):
response = self.session.get(self.METADATA_API_URL.format(type="me", item_id=""))
check_response(response)
return response.text
self.user_profile = response.json()
@staticmethod
def media_id_to_gid(media_id: str) -> str:
+29
View File
@@ -0,0 +1,29 @@
import hashlib
import hmac
import math
# thanks to https://github.com/glomatico/votify/pull/42#issuecomment-2727036757
class TOTP:
def __init__(self) -> None:
# dumped directly from the object, after all decryptions
self.secret = b"5507145853487499592248630329347"
self.version = 5
self.period = 30
self.digits = 6
def generate(self, timestamp: int) -> str:
counter = math.floor(timestamp / 1000 / self.period)
counter_bytes = counter.to_bytes(8, byteorder="big")
h = hmac.new(self.secret, counter_bytes, hashlib.sha1)
hmac_result = h.digest()
offset = hmac_result[-1] & 0x0F
binary = (
(hmac_result[offset] & 0x7F) << 24
| (hmac_result[offset + 1] & 0xFF) << 16
| (hmac_result[offset + 2] & 0xFF) << 8
| (hmac_result[offset + 3] & 0xFF)
)
return str(binary % (10**self.digits)).zfill(self.digits)