├── selenium_youtube ├── enums │ ├── __init__.py │ ├── visibility.py │ ├── analytics_tab.py │ ├── analytics_period.py │ └── upload_status.py ├── __init__.py └── youtube.py ├── requirements.txt ├── demo.py ├── LICENSE ├── setup.py ├── .gitignore └── README.md /selenium_youtube/enums/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.10.0 2 | kcu==0.0.73 3 | kstopit==0.0.16 4 | kyoutubescraper==0.0.2 5 | noraise==0.0.16 6 | selenium==4.0.0 7 | selenium-browser==0.0.12 8 | selenium-chrome==0.0.29 9 | selenium-firefox==2.0.8 10 | selenium-uploader-account==0.2.3 11 | setuptools==67.8.0 12 | xpath-utils==0.0.3 -------------------------------------------------------------------------------- /selenium_youtube/__init__.py: -------------------------------------------------------------------------------- 1 | from .youtube import Youtube 2 | 3 | from .enums.analytics_period import AnalyticsPeriod 4 | from .enums.analytics_tab import AnalyticsTab 5 | from .enums.visibility import Visibility 6 | 7 | from kyoutubescraper import ChannelAboutData, YoutubeScraper as Scraper 8 | from selenium_uploader_account import * -------------------------------------------------------------------------------- /demo.py: -------------------------------------------------------------------------------- 1 | from selenium_youtube import Youtube 2 | 3 | # pip install selenium_firefox 4 | from selenium_firefox import Firefox 5 | firefox = Firefox() 6 | 7 | # pip install selenium_chrome 8 | from selenium_chrome import Chrome 9 | chrome = Chrome() 10 | 11 | youtube = Youtube( 12 | browser=chrome # or firefox 13 | ) 14 | 15 | upload_result = youtube.upload('path_to_video', 'title', 'description', ['tag1', 'tag2']) -------------------------------------------------------------------------------- /selenium_youtube/enums/visibility.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------ Imports ----------------------------------------------------------- # 2 | 3 | # System 4 | from enum import Enum, auto 5 | 6 | # -------------------------------------------------------------------------------------------------------------------------------- # 7 | 8 | 9 | 10 | # ------------------------------------------------------- class: Visibility ------------------------------------------------------ # 11 | 12 | class Visibility(Enum): 13 | PRIVATE = auto() 14 | UNLISTED = auto() 15 | PUBLIC = auto() 16 | 17 | # -------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------- /selenium_youtube/enums/analytics_tab.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------ Imports ----------------------------------------------------------- # 2 | 3 | # System 4 | from enum import Enum, auto 5 | 6 | # -------------------------------------------------------------------------------------------------------------------------------- # 7 | 8 | 9 | 10 | # ------------------------------------------------------ class: AnalyticsTab ----------------------------------------------------- # 11 | 12 | class AnalyticsTab(Enum): 13 | OVERVIEW = 'overview' 14 | REACH = 'reach_viewers' 15 | ENGAGEMENT = 'interest_viewers' 16 | AUDIENCE = 'build_audience' 17 | 18 | # -------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------- /selenium_youtube/enums/analytics_period.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------ Imports ----------------------------------------------------------- # 2 | 3 | # System 4 | from enum import Enum, auto 5 | 6 | # -------------------------------------------------------------------------------------------------------------------------------- # 7 | 8 | 9 | 10 | # ---------------------------------------------------- class: AnalyticsPeriod ---------------------------------------------------- # 11 | 12 | class AnalyticsPeriod(Enum): 13 | LAST_7_DAYS = 'week' 14 | LAST_28_DAYS = '4_weeks' 15 | LAST_90_DAYS = 'quarter' 16 | LAST_365_DAYS = 'year' 17 | LIFETIME = 'lifetime' 18 | 19 | # -------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kristóf-Attila Kovács 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools, os 2 | 3 | readme_path = 'README.md' 4 | 5 | if os.path.exists(readme_path): 6 | with open(readme_path, 'r') as f: 7 | long_description = f.read() 8 | else: 9 | long_description = 'selenium_youtube' 10 | 11 | setuptools.setup( 12 | name='selenium_youtube', 13 | version='2.0.31', 14 | author="Kovács Kristóf-Attila & Péntek Zsolt", 15 | description='selenium_youtube', 16 | long_description=long_description, 17 | long_description_content_type='text/markdown', 18 | url="https://github.com/kkristof200/selenium_youtube", 19 | packages=setuptools.find_packages(), 20 | install_requires=[ 21 | 'beautifulsoup4>=4.10.0', 22 | 'kcu>=0.0.73', 23 | 'kstopit>=0.0.16', 24 | 'kyoutubescraper>=0.0.2', 25 | 'noraise>=0.0.16', 26 | 'selenium>=4.0.0', 27 | 'selenium-browser>=0.0.12', 28 | 'selenium-chrome>=0.0.29', 29 | 'selenium-firefox>=2.0.8', 30 | 'selenium-uploader-account>=0.2.3', 31 | 'setuptools>=67.8.0', 32 | 'xpath-utils>=0.0.3' 33 | ], 34 | classifiers=[ 35 | 'Programming Language :: Python :: 3.4', 36 | 'Programming Language :: Python :: 3.5', 37 | 'Programming Language :: Python :: 3.6', 38 | 'Programming Language :: Python :: 3.7', 39 | 'Programming Language :: Python :: 3.8', 40 | 'Programming Language :: Python :: 3.9', 41 | 'License :: OSI Approved :: MIT License', 42 | 'Operating System :: OS Independent', 43 | ], 44 | python_requires='>=3.4' 45 | ) -------------------------------------------------------------------------------- /selenium_youtube/enums/upload_status.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------ Imports ----------------------------------------------------------- # 2 | 3 | # System 4 | from enum import Enum 5 | 6 | # Pip 7 | from selenium_firefox.firefox import Firefox 8 | 9 | # -------------------------------------------------------------------------------------------------------------------------------- # 10 | 11 | 12 | 13 | # ------------------------------------------------------ class: UploadStatus ----------------------------------------------------- # 14 | 15 | class UploadStatus(Enum): 16 | UNIDENTIFIED = -1 17 | UPLOADING = 0 18 | PROCESSING_SD = 1 19 | PROCESSED_SD_PROCESSING_HD = 2 20 | PROCESSED_ALL = 3 21 | 22 | # --------------------------------------------------------- Init --------------------------------------------------------- # 23 | 24 | @classmethod 25 | def get_status( 26 | cls, 27 | ff: Firefox, 28 | element 29 | ): 30 | attriutes = ff.get_attributes(element) 31 | 32 | uploading = 'uploading' in attriutes and attriutes['uploading'] == '' 33 | processing = 'processing' in attriutes and attriutes['processing'] == '' 34 | processed = 'checks-can-start' in attriutes and attriutes['checks-can-start'] == '' 35 | 36 | if uploading: 37 | return UploadStatus.UPLOADING 38 | elif processing and processed: 39 | return UploadStatus.PROCESSED_SD_PROCESSING_HD 40 | elif processing: 41 | return UploadStatus.PROCESSING_SD 42 | elif processed: 43 | return UploadStatus.PROCESSED_ALL 44 | 45 | return UploadStatus.UNIDENTIFIED 46 | 47 | 48 | # -------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # selenium_youtube 2 | 3 | ![PyPI - package version](https://img.shields.io/pypi/v/selenium_youtube?logo=pypi&style=flat-square) 4 | ![PyPI - license](https://img.shields.io/pypi/l/selenium_youtube?label=package%20license&style=flat-square) 5 | ![PyPI - python version](https://img.shields.io/pypi/pyversions/selenium_youtube?logo=pypi&style=flat-square) 6 | ![PyPI - downloads](https://img.shields.io/pypi/dm/selenium_youtube?logo=pypi&style=flat-square) 7 | 8 | ![GitHub - last commit](https://img.shields.io/github/last-commit/kkristof200/selenium_youtube?style=flat-square) 9 | ![GitHub - commit activity](https://img.shields.io/github/commit-activity/m/kkristof200/selenium_youtube?style=flat-square) 10 | 11 | ![GitHub - code size in bytes](https://img.shields.io/github/languages/code-size/kkristof200/selenium_youtube?style=flat-square) 12 | ![GitHub - repo size](https://img.shields.io/github/repo-size/kkristof200/selenium_youtube?style=flat-square) 13 | ![GitHub - lines of code](https://img.shields.io/tokei/lines/github/kkristof200/selenium_youtube?style=flat-square) 14 | 15 | ![GitHub - license](https://img.shields.io/github/license/kkristof200/selenium_youtube?label=repo%20license&style=flat-square) 16 | 17 | ## Description 18 | 19 | selenium implementation of youtube, which can upload/watch/like/comment/pin comment on videos 20 | 21 | ## Install 22 | 23 | ~~~~bash 24 | pip install selenium_youtube 25 | # or 26 | pip3 install selenium_youtube 27 | ~~~~ 28 | 29 | ## Usage 30 | 31 | ~~~~python 32 | from selenium_youtube import Youtube 33 | 34 | # pip install selenium_firefox 35 | from selenium_firefox import Firefox 36 | firefox = Firefox() 37 | 38 | # pip install selenium_chrome 39 | from selenium_chrome import Chrome 40 | chrome = Chrome() 41 | 42 | youtube = Youtube( 43 | browser=chrome # or firefox 44 | ) 45 | 46 | upload_result = youtube.upload('path_to_video', 'title', 'description', ['tag1', 'tag2']) 47 | ~~~~ 48 | 49 | ## Dependencies 50 | 51 | [beautifulsoup4](https://pypi.org/project/beautifulsoup4), [kcu](https://pypi.org/project/kcu), [kstopit](https://pypi.org/project/kstopit), [kyoutubescraper](https://pypi.org/project/kyoutubescraper), [noraise](https://pypi.org/project/noraise), [selenium](https://pypi.org/project/selenium), [selenium-browser](https://pypi.org/project/selenium-browser), [selenium-chrome](https://pypi.org/project/selenium-chrome), [selenium-firefox](https://pypi.org/project/selenium-firefox), [selenium-uploader-account](https://pypi.org/project/selenium-uploader-account), [setuptools](https://pypi.org/project/setuptools), [xpath-utils](https://pypi.org/project/xpath-utils) 52 | 53 | 54 | 55 | ## Credits 56 | 57 | [Péntek Zsolt](https://github.com/Zselter07) -------------------------------------------------------------------------------- /selenium_youtube/youtube.py: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------ Imports ----------------------------------------------------------- # 2 | 3 | # System 4 | from pickle import NONE 5 | from typing import List, Optional, Tuple, Callable, Union 6 | import time 7 | import os 8 | from sys import platform 9 | 10 | # Pip 11 | from selenium_uploader_account import SeleniumUploaderAccount 12 | from selenium_browser import Browser 13 | from noraise import noraise 14 | from kcu import strings 15 | from kstopit import timeoutable, TimeoutType 16 | from kyoutubescraper import YoutubeScraper, ChannelAboutData 17 | 18 | from selenium.webdriver.common.by import By 19 | from selenium.webdriver.common.keys import Keys 20 | 21 | from bs4 import BeautifulSoup as bs 22 | from xpath_utils.models.enums.xpath_condition_relation import XPathConditionRelation 23 | from xpath_utils.models.enums.xpath_condition_type import XPathConditionType 24 | from xpath_utils.models.xpath_condition import XPathCondition 25 | 26 | # Local 27 | from .enums.visibility import Visibility 28 | from .enums.upload_status import UploadStatus 29 | from .enums.analytics_period import AnalyticsPeriod 30 | from .enums.analytics_tab import AnalyticsTab 31 | 32 | # -------------------------------------------------------------------------------------------------------------------------------- # 33 | 34 | 35 | 36 | # ------------------------------------------------------------ Defines ----------------------------------------------------------- # 37 | 38 | YT_URL = 'https://www.youtube.com' 39 | YT_STUDIO_URL = 'https://studio.youtube.com' 40 | YT_UPLOAD_URL = 'https://www.youtube.com/upload' 41 | YT_LOGIN_URL = 'https://accounts.google.com/signin/v2/identifier?service=youtube' 42 | YT_STUDIO_VIDEO_URL = 'https://studio.youtube.com/video/{}/edit/basic' 43 | YT_WATCH_VIDEO_URL = 'https://www.youtube.com/watch?v={}' 44 | YT_PROFILE_URL = 'https://www.youtube.com/channel/{}' 45 | YT_PROFILE_CONTENT_URL = 'https://studio.youtube.com/channel/{}/videos' 46 | YT_SEARCH_URL = 'https://www.youtube.com/results?search_query={}' 47 | 48 | MAX_TITLE_CHAR_LEN = 100 49 | MAX_DESCRIPTION_CHAR_LEN = 5000 50 | MAX_TAGS_CHAR_LEN = 400 51 | MAX_TAG_CHAR_LEN = 30 52 | 53 | LOGIN_INFO_COOKIE_NAME = 'LOGIN_INFO' 54 | ERROR_MAX_UPLOAD_LIMIT_REACHED = 'MAX_UPLOAD_LIMIT_REACHED' 55 | 56 | # -------------------------------------------------------------------------------------------------------------------------------- # 57 | 58 | 59 | 60 | # -------------------------------------------------------- class: Youtube -------------------------------------------------------- # 61 | 62 | class Youtube(SeleniumUploaderAccount): 63 | 64 | # --------------------------------------------------------- Init --------------------------------------------------------- # 65 | 66 | def __init__( 67 | self, 68 | 69 | # browser 70 | browser: Browser, 71 | 72 | # login 73 | prompt_user_input_login: bool = True, 74 | login_prompt_callback: Optional[Callable[[str], None]] = None, 75 | login_prompt_timeout_seconds: int = 60*5 76 | ): 77 | super().__init__( 78 | # browser 79 | browser=browser, 80 | 81 | # login 82 | prompt_user_input_login=prompt_user_input_login, 83 | login_prompt_callback=login_prompt_callback, 84 | login_prompt_timeout_seconds=login_prompt_timeout_seconds, 85 | ) 86 | 87 | if not self.did_log_in_at_init: 88 | self.__dismiss_alerts() 89 | 90 | 91 | # ------------------------------------------------------- Overrides ------------------------------------------------------ # 92 | 93 | def _upload_function(self) -> Callable: 94 | return self.upload 95 | 96 | def _home_url(self) -> str: 97 | return YT_URL 98 | 99 | def _get_current_user_id(self) -> Optional[str]: 100 | return self.get_current_channel_id() 101 | 102 | def _profile_url_format(self) -> Optional[str]: 103 | return YT_PROFILE_URL 104 | 105 | def _login_via_cookies_needed_cookie_names(self) -> Union[str, List[str]]: 106 | return LOGIN_INFO_COOKIE_NAME 107 | 108 | 109 | # ---------------------------------------------------- Public methods ---------------------------------------------------- # 110 | 111 | def get_sub_and_video_count(self, channel_id: str) -> Optional[Tuple[int, int]]: 112 | return YoutubeScraper(user_agent=self.user_agent, proxy=self.proxy.string).get_sub_and_video_count(channel_id=channel_id) 113 | 114 | def get_channel_about_data( 115 | self, 116 | user_name: Optional[str] = None, 117 | channel_id: Optional[str] = None, 118 | channel_url_name: Optional[str] = None 119 | ) -> Optional[ChannelAboutData]: 120 | return YoutubeScraper(user_agent=self.user_agent, proxy=self.proxy.string).get_channel_about_data( 121 | user_name=user_name, 122 | channel_id=channel_id, 123 | channel_url_name=channel_url_name 124 | ) 125 | 126 | def watch_video( 127 | self, 128 | video_id: str, 129 | percent_to_watch: float = -1, # 0-100 # -1 means all 130 | like: bool = False 131 | ) -> Tuple[bool, bool]: # watched, liked 132 | watched = False 133 | liked = False 134 | 135 | try: 136 | self.get(YT_WATCH_VIDEO_URL.format(video_id)) 137 | length_s = float(strings.between(self.browser.driver.page_source, 'detailpage\\\\u0026len=', '\\\\')) 138 | play_button = self.browser.find_by('button', class_='ytp-large-play-button ytp-button', timeout=0.5) 139 | 140 | if play_button and play_button.is_displayed(): 141 | play_button.click() 142 | time.sleep(1) 143 | 144 | while True: 145 | ad = self.browser.find_by('div', class_='video-ads ytp-ad-module', timeout=0.5) 146 | 147 | if not ad or not ad.is_displayed(): 148 | break 149 | 150 | time.sleep(0.1) 151 | 152 | watched = True 153 | seconds_to_watch = percent_to_watch / 100 * length_s if percent_to_watch >= 0 else length_s 154 | 155 | if seconds_to_watch > 0: 156 | self.print('Goinng to watch', seconds_to_watch) 157 | time.sleep(seconds_to_watch) 158 | 159 | return watched, self.like(video_id) if like and self.is_logged_in else False 160 | except Exception as e: 161 | self.print(e) 162 | 163 | return watched, liked 164 | 165 | def like(self, video_id: str) -> bool: 166 | if not self.is_logged_in: 167 | print('Error - \'upload\': Isn\'t logged in') 168 | 169 | return False 170 | 171 | self.get(YT_WATCH_VIDEO_URL.format(video_id)) 172 | 173 | try: 174 | buttons_container = self.browser.find_by('div', id_='top-level-buttons', class_='style-scope ytd-menu-renderer', timeout=1.5) 175 | 176 | if buttons_container: 177 | button_container = self.browser.find_by('ytd-toggle-button-renderer', class_='style-scope ytd-menu-renderer force-icon-button style-text', timeout=0.5, in_element=buttons_container) 178 | 179 | if button_container: 180 | button = self.browser.find_by('button', id_='button', timeout=0.5, in_element=button_container) 181 | 182 | if button: 183 | attr = button.get_attribute('aria-pressed') 184 | 185 | if attr and attr == 'false': 186 | button.click() 187 | 188 | return True 189 | 190 | return False 191 | except Exception as e: 192 | self.print(e) 193 | 194 | return False 195 | 196 | def upload( 197 | self, 198 | video_path: str, 199 | title: str, 200 | description: str, 201 | tags: Optional[List[str]] = None, 202 | made_for_kids: bool = False, 203 | visibility: Visibility = Visibility.PUBLIC, 204 | thumbnail_image_path: Optional[str] = None, 205 | timeout: Optional[int] = 60*3, # 3 min 206 | extra_sleep_after_upload: Optional[int] = None, 207 | extra_sleep_before_publish: Optional[int] = None 208 | ) -> (bool, Optional[str]): 209 | if not self.is_logged_in: 210 | print('Error - \'upload\': Isn\'t logged in') 211 | 212 | return False, None 213 | 214 | res = self.__upload( 215 | video_path=video_path, 216 | title=title, 217 | description=description, 218 | tags=tags, 219 | made_for_kids=made_for_kids, 220 | visibility=visibility, 221 | thumbnail_image_path=thumbnail_image_path, 222 | extra_sleep_after_upload=extra_sleep_after_upload, 223 | extra_sleep_before_publish=extra_sleep_before_publish, 224 | timeout=timeout 225 | ) 226 | 227 | if isinstance(res, Exception): 228 | self.print(res) 229 | 230 | return False, None 231 | 232 | return res 233 | 234 | def get_current_channel_id(self, _click_avatar: bool = False, _get_home_url: bool = False) -> Optional[str]: 235 | if not self.is_logged_in: 236 | print('Error - \'upload\': Isn\'t logged in') 237 | 238 | return None 239 | 240 | if _get_home_url: 241 | self.get(YT_URL) 242 | 243 | try: 244 | if _click_avatar: 245 | avatar_button = self.browser.find_by('button', id_='avatar-btn', timeout=0.5) 246 | 247 | if avatar_button: 248 | avatar_button.click() 249 | 250 | href_containers = self.browser.find_all_by('a', class_='yt-simple-endpoint style-scope ytd-compact-link-renderer', timeout=0.5) 251 | 252 | if href_containers: 253 | for href_container in href_containers: 254 | href = href_container.get_attribute('href') 255 | 256 | if href and 'channel/' in href: 257 | return strings.between(href, 'channel/', '?') 258 | except Exception as e: 259 | self.print(e) 260 | 261 | if not _click_avatar: 262 | return self.get_current_channel_id(_click_avatar=True, _get_home_url=_get_home_url) 263 | elif not _get_home_url: 264 | return self.get_current_channel_id(_click_avatar=False, _get_home_url=True) 265 | 266 | return None 267 | 268 | def load_video(self, video_id: str): 269 | self.get(self.__video_url(video_id)) 270 | 271 | def comment_on_video( 272 | self, 273 | video_id: str, 274 | comment: str, 275 | pinned: bool = False, 276 | timeout: Optional[int] = 15 277 | ) -> (bool, bool): 278 | if not self.is_logged_in: 279 | print('Error - \'upload\': Isn\'t logged in') 280 | 281 | return False, False 282 | 283 | res = self.__comment_on_video( 284 | video_id=video_id, 285 | comment=comment, 286 | pinned=pinned, 287 | timeout=timeout 288 | ) 289 | 290 | if isinstance(res, Exception): 291 | self.print(res) 292 | 293 | return False, False 294 | 295 | return res 296 | 297 | def get_channel_video_ids( 298 | self, 299 | channel_id: Optional[str] = None, 300 | ignored_titles: Optional[List[str]] = None 301 | ) -> List[str]: 302 | video_ids = [] 303 | ignored_titles = ignored_titles or [] 304 | channel_id = channel_id or self.current_user_id 305 | 306 | try: 307 | self.get(self.__channel_videos_url(channel_id)) 308 | last_page_source = self.browser.driver.page_source 309 | 310 | while True: 311 | self.browser.scroll(1500) 312 | 313 | i=0 314 | max_i = 100 315 | sleep_time = 0.1 316 | should_break = True 317 | 318 | while i < max_i: 319 | i += 1 320 | time.sleep(sleep_time) 321 | 322 | if len(last_page_source) != len(self.browser.driver.page_source): 323 | last_page_source = self.browser.driver.page_source 324 | should_break = False 325 | 326 | break 327 | 328 | if should_break: 329 | break 330 | 331 | soup = bs(self.browser.driver.page_source, 'lxml') 332 | elems = soup.find_all('a', {'id':'video-title', 'class':'yt-simple-endpoint style-scope ytd-grid-video-renderer'}) 333 | 334 | for elem in elems: 335 | if 'title' in elem.attrs: 336 | should_continue = False 337 | title = elem['title'].strip().lower() 338 | 339 | for ignored_title in ignored_titles: 340 | if ignored_title.strip().lower() == title: 341 | should_continue = True 342 | 343 | break 344 | 345 | if should_continue: 346 | continue 347 | 348 | if 'href' in elem.attrs and '/watch?v=' in elem['href']: 349 | vid_id = strings.between(elem['href'], '?v=', '&') 350 | 351 | if vid_id is not None and vid_id not in video_ids: 352 | video_ids.append(vid_id) 353 | except Exception as e: 354 | self.print(e) 355 | 356 | return video_ids 357 | 358 | def check_analytics( 359 | self, 360 | tab: AnalyticsTab = AnalyticsTab.OVERVIEW, 361 | period: AnalyticsPeriod = AnalyticsPeriod.LAST_28_DAYS 362 | ) -> bool: 363 | return self.__open_yt_studio(f'analytics/tab-{tab.value}/period-{period.value}') 364 | 365 | def check_channel_branding(self) -> bool: 366 | return self.__open_yt_studio('editing/images') 367 | 368 | def check_channel_basic_info(self) -> bool: 369 | return self.__open_yt_studio('editing/details') 370 | 371 | @noraise(default_return_value=(False, 0)) 372 | def get_violations(self) -> Tuple[bool, int]: # has_warning, strikes 373 | self.get(YT_STUDIO_URL) 374 | violations_container = self.browser.find_by('div', class_='style-scope ytcd-strikes-item') 375 | 376 | if not violations_container: 377 | return False, 0 378 | 379 | violations_label = self.browser.find_by('div', class_='label style-scope ytcp-badge', in_element=violations_container) 380 | 381 | if not violations_label: 382 | return False, 0 383 | 384 | violation_text = violations_label.text.strip().lower() 385 | violation_text_number = 0 386 | 387 | try: 388 | violation_text_number = int(violation_text) 389 | except: 390 | pass 391 | 392 | return True, violation_text_number 393 | 394 | @noraise(default_return_value=False) 395 | def add_endscreen(self, video_id: str, max_wait_seconds_for_processing: float = 0) -> bool: 396 | self.get(YT_STUDIO_VIDEO_URL.format(video_id)) 397 | start_time = time.time() 398 | 399 | while True: 400 | attrs = self.browser.get_attributes(self.browser.find_by('ytcp-text-dropdown-trigger', id_='endscreen-editor-link')) 401 | 402 | if not attrs or 'disabled' in attrs: 403 | if time.time() - start_time < max_wait_seconds_for_processing: 404 | time.sleep(1) 405 | 406 | continue 407 | 408 | return False 409 | else: 410 | break 411 | 412 | self.browser.find_by('ytcp-text-dropdown-trigger', id_='endscreen-editor-link').click() 413 | time.sleep(0.5) 414 | self.browser.find_all_by('div', class_='card style-scope ytve-endscreen-template-picker')[0].click() 415 | time.sleep(0.5) 416 | self.browser.find_by('ytcp-button', id_='save-button').click() 417 | 418 | time.sleep(2) 419 | 420 | return self.browser.find_by('ytve-endscreen-editor-options-panel', class_='style-scope ytve-editor', timeout=0.5) is None 421 | 422 | @noraise(default_return_value=False) 423 | def remove_welcome_popup( 424 | self, 425 | offset: Tuple[int, int] = (20, 20) 426 | ) -> bool: 427 | timeout = 5 428 | 429 | self.get(YT_STUDIO_URL, force=True) 430 | self.__dismiss_welcome_popup(offset=offset, timeout=timeout) 431 | 432 | self.get(YT_UPLOAD_URL, force=True) 433 | 434 | return self.__dismiss_welcome_popup(offset=offset, timeout=timeout) 435 | 436 | @noraise(default_return_value=None) 437 | def setup_account_branding( 438 | self, 439 | profile_pic: Optional[str] = None, 440 | banner_image: Optional[str] = None, 441 | watermark: Optional[str] = None, 442 | ) -> None: 443 | self.check_channel_branding() 444 | time.sleep(5) 445 | 446 | if profile_pic: 447 | print('profile_pic', profile_pic) 448 | self.browser.find_by('input', class_='style-scope ytcp-profile-image-upload', type='file', timeout=10).send_keys(profile_pic) 449 | print('sleeping') 450 | time.sleep(1) 451 | self.browser.move_to_element(element=self.browser.find_by('ytcp-button', id_='done-button', class_='done-btn style-scope ytcp-profile-image-editor', timeout=10), click=True) 452 | 453 | if banner_image: 454 | self.browser.find_by('input', class_='style-scope ytcp-banner-upload', type='file', timeout=10).send_keys(banner_image) 455 | time.sleep(1) 456 | self.browser.move_to_element(element=self.browser.find_by('ytcp-button', id_='done-button', class_='done-btn style-scope ytcp-banner-editor', timeout=10), click=True) 457 | 458 | if watermark: 459 | self.browser.find_by('input', class_='style-scope ytcp-video-watermark-upload', type='file', timeout=10).send_keys(watermark) 460 | time.sleep(1) 461 | self.browser.move_to_element(element=self.browser.find_by('ytcp-button', id_='done-button', class_='done-btn style-scope ytcp-video-watermark-image-editor', timeout=10), click=True) 462 | 463 | self.browser.move_to_element(element=self.browser.find_by('ytcp-button', id_='publish-button'), click=True) 464 | 465 | @noraise(default_return_value=None) 466 | def setup_account_details( 467 | self, 468 | name: Optional[str] = None, 469 | description: Optional[str] = None 470 | ) -> None: 471 | self.check_channel_basic_info() 472 | time.sleep(5) 473 | 474 | if name: 475 | self.browser.find_by('ytcp-icon-button', id_='edit-button', class_='style-scope ytcp-channel-editing-channel-name', timeout=10).click() 476 | self.browser.set_textfield_text_remove_old( 477 | element=self.browser.find_by('input', id_='brand-name-input', timeout=5), 478 | text=name 479 | ) 480 | 481 | if description: 482 | self.browser.set_textfield_text_remove_old( 483 | element=self.browser.find_by('div', id_='textbox', timeout=5) or self.browser.find_by(id_='textbox', timeout=10), 484 | text=description 485 | ) 486 | 487 | self.browser.move_to_element(element=self.browser.find_by('ytcp-button', id_='publish-button'), click=True) 488 | 489 | 490 | # ---------------------------------------------------- Private methods --------------------------------------------------- # 491 | 492 | @noraise(default_return_value=False) 493 | def __open_yt_studio( 494 | self, 495 | sub_url: str 496 | ) -> bool: 497 | if not self.current_user_id: 498 | self.print('No channel ID found') 499 | 500 | return False 501 | 502 | _YT_STUDIO_URL = YT_STUDIO_URL.rstrip('/') 503 | sub_url = sub_url.lstrip('/') 504 | 505 | self.get(f'{_YT_STUDIO_URL}/channel/{self.current_user_id}/{sub_url}') 506 | 507 | return True 508 | 509 | def _input_file( 510 | self, 511 | file_path: str, 512 | ) -> None: 513 | # can throw 514 | self.browser.find_by('input', type='file').send_keys(file_path) 515 | 516 | try: 517 | self.browser.move_to_element(element=self.browser.find_by('ytcp-button', id_='publish-button'), click=True) 518 | except: 519 | print('_input_file, move to element self.browser.find_by(\'ytcp-button\', id_=\'publish-button\')') 520 | 521 | @timeoutable(timeout_type=TimeoutType.Threading if os.name == 'nt' else TimeoutType.Signal, name='Upload') 522 | def __upload( 523 | self, 524 | video_path: str, 525 | title: str, 526 | description: str, 527 | tags: Optional[List[str]] = None, 528 | made_for_kids: bool = False, 529 | visibility: Visibility = Visibility.PUBLIC, 530 | thumbnail_image_path: Optional[str] = None, 531 | extra_sleep_after_upload: Optional[int] = None, 532 | extra_sleep_before_publish: Optional[int] = None, 533 | extra_sleep_after_publish: Optional[float] = 15, 534 | timeout: Optional[int] = None 535 | ) -> (bool, Optional[str]): 536 | self.get(YT_URL) 537 | time.sleep(1.5) 538 | 539 | try: 540 | self.get(YT_UPLOAD_URL) 541 | time.sleep(1.5) 542 | self.save_cookies() 543 | 544 | self.__dismiss_welcome_popups(timeout=3) 545 | # self.print('__dismiss_dialogs') 546 | # self.__dismiss_dialogs(timeout=1) 547 | self.__dismiss_callouts(timeout=1) 548 | 549 | self._input_file(video_path) 550 | self.print('Upload: uploaded video') 551 | 552 | if extra_sleep_after_upload is not None and extra_sleep_after_upload > 0: 553 | self.print(f'Sleeping extra {extra_sleep_after_upload} seconds') 554 | time.sleep(extra_sleep_after_upload) 555 | 556 | self.__dismiss_welcome_popups(timeout=1) 557 | # self.print('__dismiss_dialogs') 558 | # self.__dismiss_dialogs(timeout=1) 559 | self.__dismiss_callouts(timeout=1) 560 | 561 | error_dialog = self.browser.find_by('div', class_='error-short style-scope ytcp-uploads-dialog', timeout=10) 562 | 563 | if error_dialog is not None: 564 | error_text = error_dialog.text 565 | 566 | if error_text and error_text.strip() != '': 567 | return False, ERROR_MAX_UPLOAD_LIMIT_REACHED 568 | 569 | self.__dismiss_welcome_popups(timeout=2) 570 | # self.__dismiss_dialogs(timeout=1) 571 | self.__dismiss_callouts(timeout=1) 572 | 573 | self.browser.set_textfield_text_remove_old( 574 | element=self.browser.find_by('div', id_='textbox', timeout=5) or self.browser.find_by(id_='textbox', timeout=5), 575 | text=title[:MAX_TITLE_CHAR_LEN] 576 | ) 577 | self.print('Upload: added title') 578 | 579 | description_container = self.browser.find_by('ytcp-mention-textbox', class_='description-textarea style-scope ytcp-uploads-basics') or self.browser.find_by('div', id='description-container') 580 | self.browser.set_textfield_text_remove_old( 581 | element=self.browser.find_by(id='textbox', in_element=description_container), 582 | text=description[:MAX_DESCRIPTION_CHAR_LEN] 583 | ) 584 | self.print('Upload: added description') 585 | 586 | if thumbnail_image_path is not None: 587 | try: 588 | self.browser.find(By.XPATH, "//input[@id='file-loader']").send_keys(thumbnail_image_path) 589 | time.sleep(0.5) 590 | self.print('Upload: added thumbnail') 591 | except Exception as e: 592 | self.print('Upload: Thumbnail error: ', e) 593 | 594 | (self.browser.find_by('ytcp-button', class_='advanced-button style-scope ytcp-uploads-details') or self.browser.find_by('ytcp-button', {"id":"toggle-button", "class":"style-scope ytcp-video-metadata-editor", "role":"button"})).click() 595 | 596 | self.print("Upload: clicked more options") 597 | self.__dismiss_welcome_popups(timeout=2) 598 | # self.__dismiss_dialogs(timeout=1) 599 | self.__dismiss_callouts(timeout=1) 600 | 601 | if tags: 602 | tags_container = self.browser.find_by('ytcp-form-input-container', id='tags-container') or self.browser.find(By.XPATH, "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-details/div/ytcp-uploads-advanced/ytcp-form-input-container/div[1]/div[2]/ytcp-free-text-chip-bar/ytcp-chip-bar/div") 603 | tags_field = self.browser.find(By.ID, 'text-input', tags_container) 604 | tags_field.send_keys(','.join([t for t in tags if len(t) <= MAX_TAG_CHAR_LEN])[:MAX_TAGS_CHAR_LEN-1] + ',') 605 | self.print("Upload: added tags") 606 | 607 | kids_selection_name = 'VIDEO_MADE_FOR_KIDS_MFK' if made_for_kids else 'VIDEO_MADE_FOR_KIDS_NOT_MFK' 608 | kids_section = self.browser.find(By.NAME, kids_selection_name) 609 | self.browser.find(By.ID, 'radioLabel', kids_section).click() 610 | self.print('Upload: did set', kids_selection_name) 611 | 612 | self.browser.find_by('ytcp-button', id='next-button').click() 613 | self.print('Upload: clicked first next') 614 | # self.__dismiss_dialogs(timeout=1) 615 | self.__dismiss_callouts(timeout=1) 616 | 617 | self.browser.find_by('ytcp-button', id='next-button').click() 618 | self.print('Upload: clicked second next') 619 | # self.__dismiss_dialogs(timeout=1) 620 | self.__dismiss_callouts(timeout=1) 621 | 622 | visibility_tab = self.browser.find_by('button', {'test-id':'REVIEW', 'state':'active'}) 623 | 624 | if not visibility_tab: 625 | for i in range(5): 626 | third_next = self.browser.find_by('ytcp-button', id='next-button', timeout=2) 627 | 628 | if third_next is not None: 629 | aria_disabled_attr = third_next.get_attribute('aria-disabled') 630 | print(f'third_next: aria-disabled: {aria_disabled_attr} | is_displayed: {third_next.is_displayed()} | is_enabled: {third_next.is_enabled()}') 631 | 632 | if aria_disabled_attr is not None: 633 | aria_disabled = aria_disabled_attr == 'true' or aria_disabled_attr == True 634 | 635 | if not aria_disabled: 636 | self.print('Upload: Clicking third next') 637 | 638 | try: 639 | third_next.click() 640 | except: 641 | print('third click not clickable') 642 | time.sleep(3) 643 | 644 | continue 645 | 646 | self.print('Upload: Clicked third next') 647 | time.sleep(3) 648 | 649 | break 650 | 651 | time.sleep(3) 652 | 653 | visibility_main_button = self.browser.find(By.NAME, visibility.name, timeout=5) 654 | self.browser.find(By.ID, 'radioLabel', visibility_main_button, timeout=5).click() 655 | self.print('Upload: set to', visibility.name) 656 | 657 | time.sleep(1) 658 | got_it_popup = self.browser.find(By.ID, 'got-it-button') 659 | 660 | if got_it_popup: 661 | print('FOUND GOT IT POPUP') 662 | got_it_popup.click() 663 | print('CLICKED GOT IT POPUP') 664 | 665 | try: 666 | video_url_container = self.browser.find(By.XPATH, "//span[@class='video-url-fadeable style-scope ytcp-video-info']", timeout=2.5) 667 | video_url_element = self.browser.find(By.XPATH, "//a[@class='style-scope ytcp-video-info']", element=video_url_container, timeout=2.5) 668 | video_id = video_url_element.get_attribute('href').split('/')[-1] 669 | except Exception as e: 670 | self.print(e) 671 | video_id = None 672 | 673 | i=0 674 | 675 | if extra_sleep_before_publish is not None and extra_sleep_before_publish > 0: 676 | time.sleep(extra_sleep_before_publish) 677 | 678 | while True: 679 | try: 680 | upload_progress_element = self.browser.find_by( 681 | 'ytcp-video-upload-progress', 682 | class_='style-scope ytcp-uploads-dialog', 683 | timeout=0.2 684 | ) 685 | 686 | upload_status = UploadStatus.get_status(self.browser, upload_progress_element) 687 | 688 | if upload_status in [UploadStatus.PROCESSING_SD, UploadStatus.PROCESSED_SD_PROCESSING_HD, UploadStatus.PROCESSED_ALL]: 689 | done_button = self.browser.find(By.ID, 'done-button') 690 | 691 | if done_button.get_attribute('aria-disabled') == 'false': 692 | done_button.click() 693 | 694 | self.print('Upload: published') 695 | 696 | if extra_sleep_after_publish: 697 | time.sleep(extra_sleep_after_publish) 698 | 699 | self.get(YT_URL) 700 | 701 | return True, video_id 702 | except Exception as e: 703 | self.print(e) 704 | i += 1 705 | 706 | if i >= 20: 707 | done_button = self.browser.find(By.ID, 'done-button') 708 | 709 | if done_button.get_attribute('aria-disabled') == 'false': 710 | done_button.click() 711 | 712 | self.print('Upload: published') 713 | 714 | time.sleep(3) 715 | self.get(YT_URL) 716 | 717 | return True, video_id 718 | 719 | raise 720 | 721 | time.sleep(1) 722 | except Exception as e: 723 | self.print(e) 724 | 725 | self.get(YT_URL) 726 | 727 | return False, None 728 | 729 | # returns (commented_successfully, pinned_comment_successfully) 730 | @timeoutable(timeout_type=TimeoutType.Threading if os.name == 'nt' else TimeoutType.Signal, name='Comment') 731 | def __comment_on_video( 732 | self, 733 | video_id: str, 734 | comment: str, 735 | pinned: bool = False, 736 | timeout: Optional[int] = None 737 | ) -> (bool, bool): 738 | self.load_video(video_id) 739 | time.sleep(1) 740 | self.browser.scroll(150) 741 | time.sleep(1) 742 | self.browser.scroll(100) 743 | time.sleep(1) 744 | self.browser.scroll(100) 745 | 746 | try: 747 | # time.sleep(10000) 748 | header = self.browser.find_by('div', id_='masthead-container', class_='style-scope ytd-app') 749 | 750 | self.print('comment: looking for \'comment_placeholder_area\'') 751 | comment_placeholder_area = self.browser.find_by('div', id_='placeholder-area', timeout=5) 752 | 753 | self.print('comment: scrollinng to \'comment_placeholder_area\'') 754 | self.browser.scroll_to_element(comment_placeholder_area, header_element=header) 755 | time.sleep(0.5) 756 | 757 | self.print('comment: getting focus') 758 | try: 759 | self.browser.find_by('div', id_='simple-box', class_='style-scope ytd-comments-header-renderer',timeout=0.5).click() 760 | self.browser.find_by('ytd-comment-simplebox-renderer', class_='style-scope ytd-comments-header-renderer',timeout=0.5).click() 761 | # comment_placeholder_area.click() 762 | self.browser.find_by('div', id_='placeholder-area', timeout=0.5).click() 763 | except Exception as e: 764 | self.print(e) 765 | 766 | self.print('comment: sending keys') 767 | # self.browser.find_by('div', id_='contenteditable-root', timeout=0.5).click() 768 | self.browser.find_by('div', id_='contenteditable-root', timeout=0.5).send_keys(comment) 769 | 770 | self.print('comment: clicking post_comment') 771 | self.browser.find_by('ytd-button-renderer', id_='submit-button', class_='style-scope ytd-commentbox style-primary size-default',timeout=0.5).click() 772 | 773 | # self.browser.find(By.XPATH, "//ytd-button-renderer[@id='submit-button' and @class='style-scope ytd-commentbox style-primary size-default']", timeout=0.5).click() 774 | 775 | if not pinned: 776 | return True, False 777 | 778 | try: 779 | try: 780 | dropdown_menu = self.browser.find_by('yt-sort-filter-sub-menu-renderer', class_='style-scope ytd-comments-header-renderer') 781 | self.browser.scroll_to_element(dropdown_menu, header_element=header) 782 | time.sleep(0.5) 783 | 784 | self.print('comment: clicking dropdown_trigger (open)') 785 | self.browser.find_by('paper-button', id_='label', class_='dropdown-trigger style-scope yt-dropdown-menu', in_element=dropdown_menu, timeout=2.5).click() 786 | 787 | try: 788 | dropdown_menu = self.browser.find_by('paper-button', id_='label', class_='dropdown-trigger style-scope yt-dropdown-menu', in_element=dropdown_menu, timeout=2.5) 789 | dropdown_elements = [elem for elem in self.browser.find_all_by('a', in_element=dropdown_menu, timeout=2.5) if 'yt-dropdown-menu' in elem.get_attribute('class')] 790 | 791 | last_dropdown_element = dropdown_elements[-1] 792 | 793 | if last_dropdown_element.get_attribute('aria-selected') == 'false': 794 | time.sleep(0.25) 795 | self.print('comment: clicking last_dropdown_element') 796 | last_dropdown_element.click() 797 | else: 798 | self.print('comment: clicking dropdown_trigger (close) (did not click last_dropdown_element (did not find it))') 799 | self.browser.find_by('paper-button', id_='label', class_='dropdown-trigger style-scope yt-dropdown-menu', in_element=dropdown_menu, timeout=2.5).click() 800 | except Exception as e: 801 | self.print(e) 802 | self.browser.find_by('paper-button', id_='label', class_='dropdown-trigger style-scope yt-dropdown-menu', in_element=dropdown_menu, timeout=2.5).click() 803 | except Exception as e: 804 | self.print(e) 805 | 806 | # self.browser.scroll(100) 807 | time.sleep(2.5) 808 | 809 | for comment_thread in self.browser.find_all_by('ytd-comment-thread-renderer', class_='style-scope ytd-item-section-renderer'): 810 | pinned_element = self.browser.find_by('yt-icon', class_='style-scope ytd-pinned-comment-badge-renderer', in_element=comment_thread, timeout=0.5) 811 | pinned = pinned_element is not None and pinned_element.is_displayed() 812 | 813 | if pinned: 814 | continue 815 | 816 | try: 817 | # button_3_dots 818 | button_3_dots = self.browser.find_by('yt-icon-button', id_='button', class_='dropdown-trigger style-scope ytd-menu-renderer', in_element=comment_thread, timeout=2.5) 819 | 820 | self.browser.scroll_to_element(button_3_dots, header_element=header) 821 | time.sleep(0.5) 822 | self.print('comment: clicking button_3_dots') 823 | button_3_dots.click() 824 | 825 | popup_renderer_3_dots = self.browser.find_by('ytd-menu-popup-renderer', class_='ytd-menu-popup-renderer', timeout=2) 826 | time.sleep(1.5) 827 | 828 | try: 829 | self.browser.driver.execute_script("arguments[0].scrollIntoView();", self.browser.find_by('a',class_='yt-simple-endpoint style-scope ytd-menu-navigation-item-renderer', in_element=popup_renderer_3_dots, timeout=2.5)) 830 | 831 | self.browser.find_by('a',class_='yt-simple-endpoint style-scope ytd-menu-navigation-item-renderer', in_element=popup_renderer_3_dots, timeout=2.5).click() 832 | except: 833 | try: 834 | self.browser.find_by('ytd-menu-navigation-item-renderer',class_='style-scope ytd-menu-popup-renderer', in_element=popup_renderer_3_dots, timeout=2.5).click() 835 | except Exception as e: 836 | try: 837 | self.browser.find_by('paper-item',class_='style-scope ytd-menu-navigation-item-renderer', in_element=popup_renderer_3_dots, timeout=2.5).click() 838 | except Exception as e: 839 | pass 840 | 841 | # confirm button 842 | confirm_button = self.browser.find_by('yt-button-renderer', id_='confirm-button', timeout=5) 843 | self.print(f'comment: clicking confirm_button ({confirm_button})') 844 | confirm_button.click() 845 | time.sleep(2) 846 | 847 | return True, True 848 | except Exception as e: 849 | self.print(e) 850 | 851 | return True, False 852 | except Exception as e: 853 | self.print(e) 854 | 855 | return True, False 856 | 857 | # could not find new comment 858 | self.print('no_new_comments') 859 | return True, False 860 | except Exception as e: 861 | self.print('comment error:', e) 862 | 863 | return False, False 864 | 865 | def __dismiss_alerts(self): 866 | dismiss_button_container = self.browser.find_by('div', id_='dismiss-button', timeout=1.5) 867 | 868 | if dismiss_button_container: 869 | dismiss_button = self.browser.find_by('paper-button', id_='button', timeout=0.5, in_element=dismiss_button_container) 870 | 871 | if dismiss_button: 872 | dismiss_button.click() 873 | 874 | iframe = self.browser.find_by('iframe', class_='style-scope ytd-consent-bump-lightbox', timeout=2.5) 875 | 876 | if iframe: 877 | self.browser.driver.switch_to.frame(iframe) 878 | 879 | agree_button = self.browser.find_by('div', id_='introAgreeButton', timeout=2.5) 880 | 881 | if agree_button: 882 | agree_button.click() 883 | 884 | if iframe: 885 | self.browser.driver.switch_to.default_content() 886 | 887 | @noraise(default_return_value=False) 888 | def __dismiss_welcome_popup( 889 | self, 890 | offset: Tuple[int, int] = (20, 20), 891 | timeout: Optional[int] = 2 892 | ) -> bool: 893 | print('__dismiss_welcome_popup') 894 | try: 895 | element = self.browser.find_by('iron-overlay-backdrop', class_='opened', timeout=timeout) 896 | 897 | if element: 898 | return self.browser.move_to_element( 899 | element=element, 900 | offset=offset, 901 | click=True 902 | ) 903 | else: 904 | return False 905 | except: 906 | return False 907 | 908 | @noraise(default_return_value=False) 909 | def __dismiss_welcome_popup_2( 910 | self, 911 | timeout: Optional[int] = 2 912 | ) -> bool: 913 | print('__dismiss_welcome_popup_2') 914 | element = self.browser.find_by('ytcp-warm-welcome-dialog', timeout=timeout) 915 | 916 | if not element: 917 | return False 918 | 919 | for i in range(5): 920 | close_button = self.browser.find_by('ytcp-button', id_='dismiss-button', timeout=1) 921 | print('close_button', close_button) 922 | 923 | if close_button is not None: 924 | try: 925 | close_button.click() 926 | return False 927 | except: 928 | pass 929 | 930 | time.sleep(1) 931 | 932 | return True 933 | 934 | @noraise(default_return_value=False) 935 | def __dismiss_welcome_popups( 936 | self, 937 | timeout: Optional[int] = 2 938 | ) -> bool: 939 | return self.__dismiss_welcome_popup_2(timeout=timeout) or self.__dismiss_welcome_popup(timeout=timeout) 940 | 941 | @noraise(default_return_value=False) 942 | def __dismiss_callouts( 943 | self, 944 | timeout: Optional[int] = 2 945 | ) -> None: 946 | print('__dismiss_callouts') 947 | element = self.browser.find_by('div', id='callout', role='dialog', timeout=timeout) 948 | 949 | while element is not None: 950 | print('dismissing callount') 951 | close_button = self.browser.find_by('ytcp-button', id='close-button', in_element=element, timeout=timeout) 952 | 953 | if close_button: 954 | close_button.click() 955 | time.sleep(4) 956 | else: 957 | break 958 | 959 | element = self.browser.find_by('div', id='callout', role='dialog', timeout=timeout) 960 | 961 | @noraise(default_return_value=None) 962 | def __dismiss_dialogs( 963 | self, 964 | timeout: Optional[int] = 2 965 | ) -> None: 966 | element = self.browser.find_by('ytcp-dialog', timeout=timeout) 967 | 968 | while element is not None: 969 | close_button = self.browser.find_by('ytcp-button', id='dismiss-button', in_element=element, timeout=timeout) 970 | 971 | if close_button: 972 | close_button.click() 973 | time.sleep(4) 974 | else: 975 | break 976 | 977 | element = self.browser.find_by('ytcp-dialog', timeout=timeout) 978 | 979 | def __video_url(self, video_id: str) -> str: 980 | return YT_URL + '/watch?v=' + video_id 981 | 982 | def __channel_videos_url(self, channel_id: str) -> str: 983 | return YT_URL + '/channel/' + channel_id + '/videos?view=0&sort=da&flow=grid' 984 | 985 | 986 | # -------------------------------------------------------------------------------------------------------------------------------- # --------------------------------------------------------------------------------