Ejemplo n.º 1
0
class Youtube:

    # ------------------------------------------------------------- Init ------------------------------------------------------------- #

    def __init__(self,
                 cookies_folder_path: str,
                 extensions_folder_path: str,
                 email: Optional[str] = None,
                 password: Optional[str] = None,
                 host: Optional[str] = None,
                 port: Optional[int] = None,
                 screen_size: Optional[Tuple[int, int]] = None,
                 full_screen: bool = False,
                 disable_images: bool = False,
                 login_prompt_callback: Optional[Callable[[str],
                                                          None]] = None):
        self.browser = Firefox(cookies_folder_path,
                               extensions_folder_path,
                               host=host,
                               port=port,
                               screen_size=screen_size,
                               full_screen=full_screen,
                               disable_images=disable_images)
        self.channel_id = None

        try:
            self.__internal_channel_id = cookies_folder_path.strip('/').split(
                '/')[-1]
        except:
            self.__internal_channel_id = cookies_folder_path

        try:
            if self.browser.login_via_cookies(
                    YT_URL, LOGIN_INFO_COOKIE_NAME) or self.login(
                        email=email,
                        password=password,
                        login_prompt_callback=login_prompt_callback):
                time.sleep(0.5)
                self.browser.get(YT_URL)
                time.sleep(0.5)
                self.browser.save_cookies()
                time.sleep(0.5)
                self.channel_id = self.get_current_channel_id()
        except Exception as e:
            print(e)
            self.quit()

            raise

    # ------------------------------------------------------ Public properties ------------------------------------------------------- #

    @property
    def is_logged_in(self) -> bool:
        return self.browser.has_cookie(LOGIN_INFO_COOKIE_NAME)

    # -------------------------------------------------------- Public methods -------------------------------------------------------- #

    def login(
            self,
            email: Optional[str],
            password: Optional[str],
            login_prompt_callback: Optional[Callable[[str],
                                                     None]] = None) -> bool:
        org_url = self.browser.driver.current_url
        self.browser.get(YT_URL)
        time.sleep(0.5)

        logged_in = self.is_logged_in

        if not logged_in and email is not None and password is not None:
            logged_in = self.__login_auto(email, password)

        if not logged_in:
            print('Could not log you in automatically.')
            logged_in = self.__login_manual(
                login_prompt_callback=login_prompt_callback)

        if logged_in:
            time.sleep(0.5)
            self.browser.get(YT_URL)
            time.sleep(0.5)

            self.browser.save_cookies()

        time.sleep(0.5)
        self.browser.get(org_url)

        return logged_in

    def upload(
        self,
        video_path: str,
        title: str,
        description: str,
        tags: List[str],
        made_for_kids: bool = False,
        visibility: Visibility = Visibility.PUBLIC,
        thumbnail_image_path: Optional[str] = None,
        _timeout: Optional[int] = 60 * 3,  # 3 min
        extra_sleep_after_upload: Optional[int] = None,
        extra_sleep_before_publish: Optional[int] = None
    ) -> (bool, Optional[str]):

        if _timeout is not None:
            try:
                with timeout.timeout(_timeout):
                    return self.__upload(
                        video_path,
                        title,
                        description,
                        tags,
                        made_for_kids=made_for_kids,
                        visibility=visibility,
                        thumbnail_image_path=thumbnail_image_path,
                        extra_sleep_after_upload=extra_sleep_after_upload,
                        extra_sleep_before_publish=extra_sleep_before_publish)
            except Exception as e:
                print('Upload', e)
                # self.browser.get(YT_URL)

                return False, None
        else:
            return self.__upload(
                video_path,
                title,
                description,
                tags,
                made_for_kids=made_for_kids,
                visibility=visibility,
                thumbnail_image_path=thumbnail_image_path,
                extra_sleep_after_upload=extra_sleep_after_upload,
                extra_sleep_before_publish=extra_sleep_before_publish)

    def get_current_channel_id(self) -> Optional[str]:
        self.browser.get(YT_URL)

        try:
            return json.loads(
                strings.between(self.browser.driver.page_source,
                                'var ytInitialGuideData = ', '};') +
                '}')['responseContext']['serviceTrackingParams'][2]['params'][
                    0]['value']
        except Exception as e:
            print('get_current_channel_id', e)

            return None

    def load_video(self, video_id: str):
        self.browser.get(self.__video_url(video_id))

    def comment_on_video(self,
                         video_id: str,
                         comment: str,
                         pinned: bool = False,
                         _timeout: Optional[int] = 15) -> (bool, bool):
        if _timeout is not None:
            try:
                with timeout.timeout(_timeout):
                    return self.__comment_on_video(video_id,
                                                   comment,
                                                   pinned=pinned)
            except Exception as e:
                print('Comment', e)
                # self.browser.get(YT_URL)

                return False, False
        else:
            return self.__comment_on_video(video_id, comment, pinned=pinned)

    def get_channel_video_ids(
            self,
            channel_id: Optional[str] = None,
            ignored_titles: Optional[List[str]] = None) -> List[str]:
        video_ids = []
        ignored_titles = ignored_titles or []

        channel_id = channel_id or self.channel_id

        try:
            self.browser.get(self.__channel_videos_url(channel_id))
            last_page_source = self.browser.driver.page_source

            while True:
                self.browser.scroll(1500)

                i = 0
                max_i = 100
                sleep_time = 0.1
                should_break = True

                while i < max_i:
                    i += 1
                    time.sleep(sleep_time)

                    if len(last_page_source) != len(
                            self.browser.driver.page_source):
                        last_page_source = self.browser.driver.page_source
                        should_break = False

                        break

                if should_break:
                    break

            soup = bs(self.browser.driver.page_source, 'lxml')
            elems = soup.find_all(
                'a', {
                    'id':
                    'video-title',
                    'class':
                    'yt-simple-endpoint style-scope ytd-grid-video-renderer'
                })

            for elem in elems:
                if 'title' in elem.attrs:
                    should_continue = False
                    title = elem['title'].strip().lower()

                    for ignored_title in ignored_titles:
                        if ignored_title.strip().lower() == title:
                            should_continue = True

                            break

                    if should_continue:
                        continue

                if 'href' in elem.attrs and '/watch?v=' in elem['href']:
                    vid_id = strings.between(elem['href'], '?v=', '&')

                    if vid_id is not None and vid_id not in video_ids:
                        video_ids.append(vid_id)
        except Exception as e:
            print(e)

        return video_ids

    def check_analytics(self) -> bool:
        self.browser.get(YT_STUDIO_URL)

        try:
            self.browser.get(
                self.browser.find(
                    By.XPATH, "//a[@id='menu-item-3']").get_attribute('href'))

            return True
        except Exception as e:
            print(e)

            return False

    def quit(self):
        self.browser.driver.quit()

    # ------------------------------------------------------- Private methods -------------------------------------------------------- #

    def __upload(
        self,
        video_path: str,
        title: str,
        description: str,
        tags: List[str],
        made_for_kids: bool = False,
        visibility: Visibility = Visibility.PUBLIC,
        thumbnail_image_path: Optional[str] = None,
        extra_sleep_after_upload: Optional[int] = None,
        extra_sleep_before_publish: Optional[int] = None
    ) -> (bool, Optional[str]):
        self.browser.get(YT_URL)
        time.sleep(1.5)

        try:
            self.browser.get(YT_UPLOAD_URL)
            time.sleep(1.5)
            self.browser.save_cookies()

            self.browser.find(By.XPATH,
                              "//input[@type='file']").send_keys(video_path)
            print('Upload: uploaded video')

            if extra_sleep_after_upload is not None and extra_sleep_after_upload > 0:
                time.sleep(extra_sleep_after_upload)

            title_field = self.browser.find_by('div', id_='textbox')
            time.sleep(0.5)
            title_field.send_keys(Keys.BACK_SPACE)
            time.sleep(0.5)
            title_field.send_keys(title[:MAX_TITLE_CHAR_LEN])
            print('Upload: added title')
            description_container = self.browser.find(
                By.XPATH,
                "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-details/div/ytcp-uploads-basics/ytcp-mention-textbox[2]"
            )
            description_field = self.browser.find(
                By.ID, "textbox", element=description_container)
            description_field.click()
            time.sleep(0.5)
            description_field.clear()
            time.sleep(0.5)
            description_field.send_keys(description[:MAX_DESCRIPTION_CHAR_LEN])
            print('Upload: added description')

            if thumbnail_image_path is not None:
                try:
                    self.browser.find(By.XPATH,
                                      "//input[@id='file-loader']").send_keys(
                                          thumbnail_image_path)
                    time.sleep(0.5)
                    print('Upload: added thumbnail')
                except Exception as e:
                    print('Upload: Thumbnail error: ', e)

            self.browser.find(
                By.XPATH,
                "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-details/div/div/ytcp-button/div"
            ).click()
            print("Upload: clicked more options")

            tags_container = 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"
            )
            tags_field = self.browser.find(By.ID, 'text-input', tags_container)
            tags_field.send_keys(','.join(tags) + ',')
            print("Upload: added tags")

            kids_selection_name = 'MADE_FOR_KIDS' if made_for_kids else 'NOT_MADE_FOR_KIDS'
            kids_section = self.browser.find(By.NAME, kids_selection_name)
            self.browser.find(By.ID, 'radioLabel', kids_section).click()
            print('Upload: did set', kids_selection_name)

            self.browser.find(By.ID, 'next-button').click()
            print('Upload: clicked first next')

            self.browser.find(By.ID, 'next-button').click()
            print('Upload: clicked second next')

            visibility_main_button = self.browser.find(By.NAME,
                                                       visibility.name)
            self.browser.find(By.ID, 'radioLabel',
                              visibility_main_button).click()
            print('Upload: set to', visibility.name)

            try:
                video_url_container = self.browser.find(
                    By.XPATH,
                    "//span[@class='video-url-fadeable style-scope ytcp-video-info']",
                    timeout=2.5)
                video_url_element = self.browser.find(
                    By.XPATH,
                    "//a[@class='style-scope ytcp-video-info']",
                    element=video_url_container,
                    timeout=2.5)
                video_id = video_url_element.get_attribute('href').split(
                    '/')[-1]
            except Exception as e:
                print(e)
                video_id = None

            i = 0

            if extra_sleep_before_publish is not None and extra_sleep_before_publish > 0:
                time.sleep(extra_sleep_before_publish)

            while True:
                try:
                    progress_label = self.browser.find_by(
                        'span', {
                            'class':
                            'progress-label style-scope ytcp-video-upload-progress'
                        },
                        timeout=0.1
                    ) or self.browser.find_by('span', {
                        'class':
                        'progress-label-old style-scope ytcp-video-upload-progress'
                    },
                                              timeout=0.1)
                    progress_label_text = progress_label.text.lower()

                    if 'finished' in progress_label_text or 'process' in progress_label_text or '100' in progress_label_text:
                        done_button = self.browser.find(By.ID, 'done-button')

                        if done_button.get_attribute(
                                'aria-disabled') == 'false':
                            done_button.click()

                            print('Upload: published')

                            time.sleep(3)
                            self.browser.get(YT_URL)

                            return True, video_id
                except Exception as e:
                    print(e)
                    i += 1

                    if i >= 20:
                        done_button = self.browser.find(By.ID, 'done-button')

                        if done_button.get_attribute(
                                'aria-disabled') == 'false':
                            done_button.click()

                            print('Upload: published')

                            time.sleep(3)
                            self.browser.get(YT_URL)

                            return True, video_id

                        raise

                time.sleep(1)
        except Exception as e:
            print(e)

            self.browser.get(YT_URL)

            return False, None

    # returns (commented_successfully, pinned_comment_successfully)
    def __comment_on_video(self,
                           video_id: str,
                           comment: str,
                           pinned: bool = False) -> (bool, bool):
        self.load_video(video_id)
        time.sleep(1)
        self.browser.scroll(150)
        time.sleep(1)
        self.browser.scroll(100)
        time.sleep(1)
        self.browser.scroll(100)

        try:
            header = self.browser.find_by('div',
                                          id_='masthead-container',
                                          class_='style-scope ytd-app')
            comment_field = self.browser.find(By.XPATH,
                                              "//div[@id='placeholder-area']",
                                              timeout=5)

            self.browser.scroll_to_element(comment_field,
                                           header_element=header)
            time.sleep(0.5)

            print('comment: clicking comment_field')
            comment_field.click()
            print('comment: sending keys')
            self.browser.find(By.XPATH,
                              "//div[@id='contenteditable-root']",
                              timeout=0.5).send_keys(comment)
            print('comment: clicking post_comment')
            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()

            if not pinned:
                return True, False

            try:
                dropdown_menu_xpath = "//yt-sort-filter-sub-menu-renderer[@class='style-scope ytd-comments-header-renderer']"
                dropdown_menu = self.browser.find(By.XPATH,
                                                  dropdown_menu_xpath)
                self.browser.scroll_to_element(dropdown_menu,
                                               header_element=header)
                time.sleep(0.5)

                dropdown_trigger_xpath = "//paper-button[@id='label' and @class='dropdown-trigger style-scope yt-dropdown-menu']"

                print('comment: clicking dropdown_trigger (open)')
                self.browser.find(By.XPATH,
                                  dropdown_trigger_xpath,
                                  element=dropdown_menu,
                                  timeout=2.5).click()

                try:
                    dropdown_menu = self.browser.find(By.XPATH,
                                                      dropdown_menu_xpath)
                    dropdown_elements = [
                        elem for elem in self.browser.find_all(
                            By.XPATH,
                            "//a",
                            element=dropdown_menu,
                            timeout=2.5)
                        if 'yt-dropdown-menu' in elem.get_attribute('class')
                    ]

                    last_dropdown_element = dropdown_elements[-1]

                    if last_dropdown_element.get_attribute(
                            'aria-selected') == 'false':
                        time.sleep(0.25)
                        print('comment: clicking last_dropdown_element')
                        last_dropdown_element.click()
                    else:
                        print(
                            'comment: clicking dropdown_trigger (close) (did not click last_dropdown_element (did not find it))'
                        )
                        self.browser.find(By.XPATH,
                                          dropdown_trigger_xpath,
                                          element=dropdown_menu,
                                          timeout=2.5).click()
                except Exception as e:
                    print(e)
                    print(
                        'comment: clicking dropdown_trigger (close) (did not click last_dropdown_element (error occured))'
                    )
                    self.browser.find(By.XPATH,
                                      dropdown_trigger_xpath,
                                      element=dropdown_menu,
                                      timeout=2.5).click()
            except Exception as e:
                print(e)

            # self.browser.scroll(100)
            time.sleep(2.5)

            for comment_thread in self.browser.find_all(
                    By.XPATH,
                    "//ytd-comment-thread-renderer[@class='style-scope ytd-item-section-renderer']"
            ):
                pinned_element = self.browser.find(
                    By.XPATH,
                    "//yt-icon[@class='style-scope ytd-pinned-comment-badge-renderer']",
                    element=comment_thread,
                    timeout=0.5)
                pinned = pinned_element is not None and pinned_element.is_displayed(
                )

                if pinned:
                    continue

                try:
                    # button_3_dots
                    button_3_dots = self.browser.find(
                        By.XPATH,
                        "//yt-icon-button[@id='button' and @class='dropdown-trigger style-scope ytd-menu-renderer']",
                        element=comment_thread,
                        timeout=2.5)

                    self.browser.scroll_to_element(button_3_dots,
                                                   header_element=header)
                    time.sleep(0.5)
                    print('comment: clicking button_3_dots')
                    button_3_dots.click()

                    popup_renderer_3_dots = self.browser.find(
                        By.XPATH,
                        "//ytd-menu-popup-renderer[@class='style-scope ytd-popup-container']",
                        timeout=2)

                    # dropdown menu item (first)
                    print(
                        'comment: clicking button_3_dots-dropdown_menu_item (to pin it)'
                    )
                    self.browser.find(
                        By.XPATH,
                        "//a[@class='yt-simple-endpoint style-scope ytd-menu-navigation-item-renderer']",
                        element=popup_renderer_3_dots,
                        timeout=2.5).click()

                    confirm_button_container = self.browser.find(
                        By.XPATH,
                        "//yt-button-renderer[@id='confirm-button' and @class='style-scope yt-confirm-dialog-renderer style-primary size-default']",
                        timeout=5)

                    # confirm button
                    print('comment: clicking confirm_button')
                    self.browser.find(
                        By.XPATH,
                        "//a[@class='yt-simple-endpoint style-scope yt-button-renderer']",
                        element=confirm_button_container,
                        timeout=2.5).click()
                    time.sleep(2)

                    return True, True
                except Exception as e:
                    print(e)

                    return True, False

            # could not find new comment
            print('no_new_comments')
            return True, False
        except Exception as e:
            print(e)

            return False, False

    def __login_auto(self, email: str, password: str) -> bool:
        self.browser.get(YT_LOGIN_URL)
        time.sleep(0.5)

        try:
            with timeout.timeout(30):
                email_field = self.browser.find_by('input', {
                    'id': 'identifierId',
                    'type': 'email'
                })
                email_field.click()
                self.browser.send_keys_delay_random(email_field, email)
                time.sleep(1)
                self.browser.find_by('div', {
                    'jscontroller': 'VXdfxd',
                    'role': 'button'
                }).click()
                time.sleep(1)

                from selenium.webdriver.common.action_chains import ActionChains
                action = ActionChains(self.browser.driver)

                pass_container = self.browser.find_by('div', {
                    'id': 'password',
                    'jscontroller': 'pxq3x'
                })
                pass_field = self.browser.find_by('input',
                                                  {'type': 'password'},
                                                  in_element=pass_container)
                action.move_to_element(pass_field).perform()
                pass_field.click()
                self.browser.send_keys_delay_random(pass_field, password)
                time.sleep(1)
                self.browser.find_by('div', {
                    'jscontroller': 'VXdfxd',
                    'role': 'button'
                }).click()
                time.sleep(1)
        except Exception as e:
            print('__login_auto', e)

        self.browser.get(YT_URL)
        time.sleep(0.5)

        return self.is_logged_in

    def __login_manual(
            self,
            login_prompt_callback: Optional[Callable[[str],
                                                     None]] = None) -> bool:
        self.browser.get(YT_LOGIN_URL)
        time.sleep(0.5)

        try:
            if login_prompt_callback is not None:
                login_prompt_callback(self.__internal_channel_id +
                                      ' needs manual input to log in.')

            input('Log in then press return')
        except Exception as e:
            print('__login_manual', e)

        self.browser.get(YT_URL)
        time.sleep(0.5)

        return self.is_logged_in

    def __video_url(self, video_id: str) -> str:
        return YT_URL + '/watch?v=' + video_id

    def __channel_videos_url(self, channel_id: str) -> str:
        return YT_URL + '/channel/' + channel_id + '/videos?view=0&sort=da&flow=grid'


# ---------------------------------------------------------------------------------------------------------------------------------------- #