diff --git a/documentation/html/youtube-music/01-search-request.json b/documentation/html/youtube-music/search/01-search-request.json similarity index 63% rename from documentation/html/youtube-music/01-search-request.json rename to documentation/html/youtube-music/search/01-search-request.json index f5bedf3..10becdf 100644 --- a/documentation/html/youtube-music/01-search-request.json +++ b/documentation/html/youtube-music/search/01-search-request.json @@ -1,3 +1,4 @@ +// https://music.youtube.com/youtubei/v1/search?key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30&prettyPrint=false // ctoken could be short for continue token { "POST": { @@ -147,3 +148,64 @@ } } + +----- + + +{ + "context": + { + "client": + { + "hl":"en", + "gl":"DE", + "remoteHost":"87.123.241.85", + "deviceMake":"", + "deviceModel":"", + "visitorData":"CgtucS1ibEdPa045ZyiT4YWmBg%3D%3D", + "userAgent":"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0,gzip(gfe)", + "clientName":"WEB_REMIX", + "clientVersion":"1.20230724.00.00-canary_experiment", + "osName":"X11","osVersion":"","originalUrl":"https://music.youtube.com/?cbrd=1","platform":"DESKTOP","clientFormFactor":"UNKNOWN_FORM_FACTOR","configInfo": + {"appInstallData":"CJPhhaYGEJ3b_hIQsdWvBRC41a8FEL22rgUQ3ravBRD-ta8FEOe6rwUQw7f-EhDgtq8FEKnErwUQ6sOvBRCst68FEIXZ_hIQ5LP-EhDMrv4SELiLrgUQ65OuBRCMt68FEOPO_hIQwt7-EhDbz68FELTJrwUQ8qivBRD4ta8FEJbOrwUQzN-uBRCPw68FEP24_RIQhLavBRC1pq8FEKqy_hIQksuvBRCa0a8FEMy3_hIQjMuvBRCj1K8FEKXC_hIQ_eeoGBD51a8F","coldConfigData":"CJPhhaYGEOy6rQUQ65OuBRC9tq4FEKT-rgUQ6KivBRDyqK8FEIy3rwUQ4bqvBRDDxq8FEJ7HrwUQ88yvBRDbz68FEMDQrwUQmtGvBRDK068FENTTrwUQo9SvBRCx1a8FELjVrwUQ-dWvBRDZ168FEI7YrwUQ0NmvBRoyQU53R2I4V013TDV5bTJ1S0hPZndFWFZqcFB4b0l6MVRxcllyNFo2dDdKVGRTQjFFS3ciMkFOd0diOFdNd0w1eW0ydUtIT2Z3RVhWanBQeG9JejFUcXJZcjRaNnQ3SlRkU0IxRUt3KkhDQU1TTUEwVGdwYW9Bc2dXX2dXZkJJOFNuUXEwQW9FRWxnTVZINUtDMEF5elI4bVVCdDhhdmxLQ0F0NWluUy1KSjQtNUJBPT0%3D","coldHashData":"CJPhhaYGEhM0OTUzOTkxMTAyODE4MjI5NTY3GJPhhaYGMjJBTndHYjhXTXdMNXltMnVLSE9md0VYVmpwUHhvSXoxVHFyWXI0WjZ0N0pUZFNCMUVLdzoyQU53R2I4V013TDV5bTJ1S0hPZndFWFZqcFB4b0l6MVRxcllyNFo2dDdKVGRTQjFFS3dCSENBTVNNQTBUZ3Bhb0FzZ1dfZ1dmQkk4U25RcTBBb0VFbGdNVkg1S0MwQXl6UjhtVUJ0OGF2bEtDQXQ1aW5TLUpKNC01QkE9PQ%3D%3D","hotHashData":"CJPhhaYGEhQxMjc1MzUxNTg3MDYwNDg5NzEwMRiT4YWmBiiU5PwSKNuT_RIoxrL9EiiqtP0SKJ6R_hIomq3-EiiUzf4SKN3O_hIo487-EiiF2f4SKJfZ_hIondv-EijI3P4SKNjd_hIovt7-EjIyQU53R2I4V013TDV5bTJ1S0hPZndFWFZqcFB4b0l6MVRxcllyNFo2dDdKVGRTQjFFS3c6MkFOd0diOFdNd0w1eW0ydUtIT2Z3RVhWanBQeG9JejFUcXJZcjRaNnQ3SlRkU0IxRUt3QihDQU1TR1EwUDJJXzVGY29BcURrVkNvM2l6UXlMN2dIRmtBRGgwQUk9"}, + "browserName":"Firefox", + "browserVersion":"115.0", + "acceptHeader":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "deviceExperimentId":"ChxOekkyTURJd056ZzBPRFF3TWpVME5EVTRNUT09EJPhhaYGGJLhhaYG", + "screenWidthPoints":923, + "screenHeightPoints":964, + "screenPixelDensity":1, + "screenDensityFloat":1, + "utcOffsetMinutes":120, + "userInterfaceTheme":"USER_INTERFACE_THEME_DARK", + "timeZone":"Atlantic/Jan_Mayen", + "musicAppInfo":{"pwaInstallabilityStatus":"PWA_INSTALLABILITY_STATUS_UNKNOWN","webDisplayMode":"WEB_DISPLAY_MODE_BROWSER","storeDigitalGoodsApiSupportStatus":{"playStoreDigitalGoodsApiSupportStatus":"DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED"}} + }, + "user":{"lockedSafetyMode":false}, + "request":{"useSsl":true,"internalExperimentFlags":[],"consistencyTokenJars":[] + }, + "adSignalsInfo":{ + "params":[ + {"key":"dt","value":"1690398867909"}, + {"key":"flash","value":"0"}, + {"key":"frm","value":"0"}, + {"key":"u_tz","value":"120"},{"key":"u_his","value":"5"},{"key":"u_h","value":"1080"},{"key":"u_w","value":"1920"},{"key":"u_ah","value":"1049"},{"key":"u_aw","value":"1866"},{"key":"u_cd","value":"24"},{"key":"bc","value":"31"},{"key":"bih","value":"964"},{"key":"biw","value":"923"},{"key":"brdim","value":"1280,31,1280,31,1866,31,1866,1049,923,964"},{"key":"vis","value":"1"},{"key":"wgl","value":"true"},{"key":"ca_type","value":"image"} + ] + } + }, + "query":"psychonaut 4", + "suggestStats":{ + "validationStatus":"VALID", + "parameterValidationStatus":"VALID_PARAMETERS", + "clientName":"youtube-music", + "searchMethod":"ENTER_KEY", + "inputMethod":"KEYBOARD", + "originalQuery":"psychonaut 4", + "availableSuggestions":[{"index":0,"suggestionType":0},{"index":1,"suggestionType":0},{"index":2,"suggestionType":0},{"index":3,"suggestionType":0},{"index":4,"suggestionType":0},{"index":5,"suggestionType":0},{"index":6,"suggestionType":0}], + "zeroPrefixEnabled":true, + "firstEditTimeMsec":1329258, + "lastEditTimeMsec":1330993 + } + } + + diff --git a/documentation/html/youtube-music/01-search-result.json b/documentation/html/youtube-music/search/01-search-result.json similarity index 100% rename from documentation/html/youtube-music/01-search-result.json rename to documentation/html/youtube-music/search/01-search-result.json diff --git a/src/actual_donwload.py b/src/actual_donwload.py index 591accb..f69b781 100644 --- a/src/actual_donwload.py +++ b/src/actual_donwload.py @@ -28,4 +28,8 @@ if __name__ == "__main__": "d: 5" ] - music_kraken.cli.download(genre="test", command_list=download_youtube_playlist, process_metadata_anyway=True) + youtube_music_test = [ + "s: psychonaut 4" + ] + + music_kraken.cli.download(genre="test", command_list=youtube_music_test, process_metadata_anyway=True) diff --git a/src/music_kraken/connection/connection.py b/src/music_kraken/connection/connection.py index 79b1d8f..edf27e9 100644 --- a/src/music_kraken/connection/connection.py +++ b/src/music_kraken/connection/connection.py @@ -53,6 +53,10 @@ class Connection: self.hearthbeat_thread = None self.hearthbeat_interval = hearthbeat_interval + @property + def user_agent(self) -> str: + return self.session.headers.get("user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36") + def start_hearthbeat(self): if self.hearthbeat_interval <= 0: diff --git a/src/music_kraken/pages/youtube_music.py b/src/music_kraken/pages/youtube_music.py index 6f4c8c6..2503bd4 100644 --- a/src/music_kraken/pages/youtube_music.py +++ b/src/music_kraken/pages/youtube_music.py @@ -1,5 +1,5 @@ from typing import Dict, List, Optional, Set, Type -from urllib.parse import urlparse +from urllib.parse import urlparse, urlunparse, quote import logging import random import json @@ -7,8 +7,9 @@ from dataclasses import dataclass import re from ..utils.exception.config import SettingValueError -from ..utils.shared import PROXIES_LIST, YOUTUBE_MUSIC_LOGGER +from ..utils.shared import PROXIES_LIST, YOUTUBE_MUSIC_LOGGER, DEBUG from ..utils.config import CONNECTION_SECTION, write_config +from ..utils.functions import get_current_millis from ..objects import Source, DatabaseObject from .abstract import Page @@ -24,6 +25,11 @@ from ..objects import ( from ..connection import Connection from ..utils.support_classes import DownloadResult + +def get_youtube_url(path: str = "", params: str = "", query: str = "", fragment: str = "") -> str: + return urlunparse(("https", "music.youtube.com", path, params, query, fragment)) + + class YoutubeMusicConnection(Connection): """ ===Hearthbeat=timings=for=YOUTUBEMUSIC=== @@ -72,20 +78,76 @@ class YouTubeMusicCredentials: # It is probably not strictly necessary, but hey :)) ctoken: str + # the context in requests + context: dict + class YoutubeMusic(Page): # CHANGE - SOURCE_TYPE = SourcePages.PRESET + SOURCE_TYPE = SourcePages.YOUTUBE LOGGER = YOUTUBE_MUSIC_LOGGER def __init__(self, *args, **kwargs): self.connection: YoutubeMusicConnection = YoutubeMusicConnection(logger=self.LOGGER) self.credentials: YouTubeMusicCredentials = YouTubeMusicCredentials( api_key=CONNECTION_SECTION.YOUTUBE_MUSIC_API_KEY.object_from_value, - ctoken="" + ctoken="", + context= { + "client": { + "hl": "en", + "gl": "DE", + "remoteHost": "87.123.241.77", + "deviceMake": "", + "deviceModel": "", + "visitorData": "CgtiTUxaTHpoXzk1Zyia59WlBg%3D%3D", + "userAgent": self.connection.user_agent, + "clientName": "WEB_REMIX", + "clientVersion": "1.20230710.01.00", + "osName": "X11", + "osVersion": "", + "originalUrl": "https://music.youtube.com/", + "platform": "DESKTOP", + "clientFormFactor": "UNKNOWN_FORM_FACTOR", + "configInfo": { + "appInstallData": "", + "coldConfigData": "", + "coldHashData": "", + "hotHashData": "" + }, + "userInterfaceTheme": "USER_INTERFACE_THEME_DARK", + "timeZone": "Atlantic/Jan_Mayen", + "browserName": "Firefox", + "browserVersion": "115.0", + "acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", + "deviceExperimentId": "ChxOekkxTmpnek16UTRNVFl4TkRrek1ETTVOdz09EJrn1aUGGJrn1aUG", + "screenWidthPoints": 584, + "screenHeightPoints": 939, + "screenPixelDensity": 1, + "screenDensityFloat": 1, + "utcOffsetMinutes": 120, + "musicAppInfo": { + "pwaInstallabilityStatus": "PWA_INSTALLABILITY_STATUS_UNKNOWN", + "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER", + "storeDigitalGoodsApiSupportStatus": { + "playStoreDigitalGoodsApiSupportStatus": "DIGITAL_GOODS_API_SUPPORT_STATUS_UNSUPPORTED" + } + } + }, + "user": { "lockedSafetyMode": False }, + "request": { + "useSsl": True, + "internalExperimentFlags": [], + "consistencyTokenJars": [] + }, + "adSignalsInfo": { + "params": [] + } + } ) - if self.credentials.api_key == "": + self.start_millis = get_current_millis() + + if self.credentials.api_key == "" or DEBUG: self._fetch_from_main_page() super().__init__(*args, **kwargs) @@ -132,12 +194,64 @@ class YoutubeMusic(Page): else: self.LOGGER.error(f"Couldn't find an API-KEY for {type(self).__name__}. :((") + # context + context_pattern = r"(?<=\"INNERTUBE_CONTEXT\":{)(.*?)(?=},\"INNERTUBE_CONTEXT_CLIENT_NAME\":)" + found_context = False + for context_string in re.findall(context_pattern, content): + try: + context = json.loads("{" + context_string + "}") + found_context + except json.decoder.JSONDecodeError: + continue + + self.credentials.context = context + break + + if not found_context: + self.LOGGER.warning(f"Couldn't find a context for {type(self).__name__}.") def get_source_type(self, source: Source) -> Optional[Type[DatabaseObject]]: return super().get_source_type(source) def general_search(self, search_query: str) -> List[DatabaseObject]: - return [] + self.LOGGER.info(f"general search for {search_query}") + print(self.credentials) + + urlescaped_query: str = quote(search_query.strip().replace(" ", "+")) + + LAST_EDITED_TIME = get_current_millis() - random.randint(0, 20) + _estimated_time = sum(len(search_query) * random.randint(50, 100) for _ in search_query.strip()) + FIRST_EDITED_TIME = LAST_EDITED_TIME - _estimated_time if LAST_EDITED_TIME - self.start_millis > _estimated_time else random.randint(50, 100) + + # construct the request + r = self.connection.post( + url=get_youtube_url(path="/youtubei/v1/search", query=f"key={self.credentials.api_key}&prettyPrint=false"), + json={ + "context": {**self.credentials.context, "adSignalsInfo":{"params":[]}}, + "query": search_query, + "suggestStats": { + "clientName": "youtube-music", + "firstEditTimeMsec": FIRST_EDITED_TIME, + "inputMethod": "KEYBOARD", + "lastEditTimeMsec": LAST_EDITED_TIME, + "originalQuery": search_query, + "parameterValidationStatus": "VALID_PARAMETERS", + "searchMethod": "ENTER_KEY", + "validationStatus": "VALID", + "zeroPrefixEnabled": True, + "availableSuggestions": [] + } + }, + headers={ + "Referer": get_youtube_url(path=f"/search", query=f"q={urlescaped_query}") + } + ) + + print(r) + + return [ + Song(title="Lore Ipsum") + ] def label_search(self, label: Label) -> List[Label]: return [] diff --git a/src/music_kraken/utils/enums/source.py b/src/music_kraken/utils/enums/source.py index 964de00..a5e213e 100644 --- a/src/music_kraken/utils/enums/source.py +++ b/src/music_kraken/utils/enums/source.py @@ -11,6 +11,7 @@ class SourceTypes(Enum): class SourcePages(Enum): YOUTUBE = "youtube" MUSIFY = "musify" + YOUTUBE_MUSIC = "youtube music" GENIUS = "genius" MUSICBRAINZ = "musicbrainz" ENCYCLOPAEDIA_METALLUM = "encyclopaedia metallum" diff --git a/src/music_kraken/utils/functions.py b/src/music_kraken/utils/functions.py index d642f2a..f773213 100644 --- a/src/music_kraken/utils/functions.py +++ b/src/music_kraken/utils/functions.py @@ -1,4 +1,11 @@ import os +from datetime import datetime + def clear_console(): - os.system('cls' if os.name in ('nt', 'dos') else 'clear') \ No newline at end of file + os.system('cls' if os.name in ('nt', 'dos') else 'clear') + + +def get_current_millis() -> int: + dt = datetime.now() + return int(dt.microsecond / 1_000) diff --git a/src/music_kraken/utils/shared.py b/src/music_kraken/utils/shared.py index 97d594f..af49658 100644 --- a/src/music_kraken/utils/shared.py +++ b/src/music_kraken/utils/shared.py @@ -126,3 +126,7 @@ FFMPEG_BINARY: Path = PATHS_SECTION.FFMPEG_BINARY.object_from_value HASNT_YET_STARTED: bool = MISC_SECTION.HASNT_YET_STARTED.object_from_value SLEEP_AFTER_YOUTUBE_403: float = CONNECTION_SECTION.SLEEP_AFTER_YOUTUBE_403.object_from_value + +DEBUG = True +if DEBUG: + print("DEBUG ACTIVE")