297 lines
12 KiB
Python
297 lines
12 KiB
Python
"""This module implements uploading videos on YouTube via Selenium using metadata JSON file
|
|
to extract its title, description etc."""
|
|
|
|
from typing import DefaultDict, Optional, Tuple
|
|
from selenium_firefox.firefox import Firefox
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.common.keys import Keys
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
import json
|
|
import time
|
|
from .Constant import *
|
|
from pathlib import Path
|
|
import logging
|
|
import platform
|
|
|
|
logging.basicConfig()
|
|
|
|
|
|
|
|
import random
|
|
|
|
#used to generate a random wait time to perform the actions on the browser
|
|
def random_time():
|
|
rand : int = random.randint(1, 3)
|
|
print("Random time: ", rand)
|
|
return rand
|
|
|
|
def load_metadata(metadata_json_path: Optional[str] = None) -> DefaultDict[str, str]:
|
|
if metadata_json_path is None:
|
|
return defaultdict(str)
|
|
with open(metadata_json_path, encoding='utf-8') as metadata_json_file:
|
|
return defaultdict(str, json.load(metadata_json_file))
|
|
|
|
|
|
class YouTubeUploader:
|
|
"""A class for uploading videos on YouTube via Selenium using metadata JSON file
|
|
to extract its title, description etc"""
|
|
|
|
def __init__(self, video_path: str, metadata_json_path: Optional[str] = None,
|
|
thumbnail_path: Optional[str] = None,
|
|
profile_path: Optional[str] = str(Path.cwd()) + "/profile",
|
|
headless : bool = True) -> None:
|
|
self.video_path = video_path
|
|
self.thumbnail_path = thumbnail_path
|
|
self.metadata_dict = load_metadata(metadata_json_path)
|
|
self.browser = Firefox(profile_path=profile_path, pickle_cookies=True, full_screen=False, headless=headless)
|
|
self.logger = logging.getLogger(__name__)
|
|
self.logger.setLevel(logging.DEBUG)
|
|
self.__validate_inputs()
|
|
print("headless for youtube =" + str(headless))
|
|
|
|
self.is_mac = False
|
|
if not any(os_name in platform.platform() for os_name in ["Windows", "Linux"]):
|
|
self.is_mac = True
|
|
|
|
self.logger.debug("Use profile path: {}".format(self.browser.source_profile_path))
|
|
|
|
def __validate_inputs(self):
|
|
if not self.metadata_dict[Constant.VIDEO_TITLE]:
|
|
self.logger.warning(
|
|
"The video title was not found in a metadata file")
|
|
self.metadata_dict[Constant.VIDEO_TITLE] = Path(
|
|
self.video_path).stem
|
|
self.logger.warning("The video title was set to {}".format(
|
|
Path(self.video_path).stem))
|
|
if not self.metadata_dict[Constant.VIDEO_DESCRIPTION]:
|
|
self.logger.warning(
|
|
"The video description was not found in a metadata file")
|
|
|
|
def upload(self):
|
|
try:
|
|
self.login()
|
|
return self.__upload()
|
|
except Exception as e:
|
|
print(e)
|
|
self.__quit()
|
|
raise
|
|
|
|
def login(self):
|
|
self.browser.get(Constant.YOUTUBE_URL)
|
|
time.sleep(random_time())
|
|
|
|
if self.browser.has_cookies_for_current_website():
|
|
self.browser.load_cookies()
|
|
self.logger.debug("Loaded cookies from {}".format(self.browser.cookies_folder_path))
|
|
time.sleep(random_time())
|
|
self.browser.refresh()
|
|
else:
|
|
self.logger.info('Please sign in and then press enter')
|
|
input()
|
|
self.browser.get(Constant.YOUTUBE_URL)
|
|
time.sleep(random_time())
|
|
self.browser.save_cookies()
|
|
self.logger.debug("Saved cookies to {}".format(self.browser.cookies_folder_path))
|
|
|
|
def __clear_field(self, field):
|
|
field.click()
|
|
time.sleep(random_time())
|
|
if self.is_mac:
|
|
field.send_keys(Keys.COMMAND + 'a')
|
|
else:
|
|
field.send_keys(Keys.CONTROL + 'a')
|
|
time.sleep(random_time())
|
|
field.send_keys(Keys.BACKSPACE)
|
|
|
|
def __write_in_field(self, field, string, select_all=False):
|
|
if select_all:
|
|
self.__clear_field(field)
|
|
else:
|
|
field.click()
|
|
time.sleep(random_time())
|
|
|
|
field.send_keys(string)
|
|
|
|
def __upload(self) -> Tuple[bool, Optional[str]]:
|
|
self.browser.get(Constant.YOUTUBE_URL)
|
|
time.sleep(random_time())
|
|
self.browser.get(Constant.YOUTUBE_UPLOAD_URL)
|
|
time.sleep(random_time())
|
|
absolute_video_path = str(Path.cwd() / self.video_path)
|
|
self.browser.find(By.XPATH, Constant.INPUT_FILE_VIDEO).send_keys(
|
|
absolute_video_path)
|
|
self.logger.debug('Attached video {}'.format(self.video_path))
|
|
|
|
# Find status container
|
|
uploading_status_container = None
|
|
while uploading_status_container is None:
|
|
time.sleep(0.5) #bug where slept too long, missed finding the element got soft locked
|
|
uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER)
|
|
|
|
if self.thumbnail_path is not None:
|
|
absolute_thumbnail_path = str(Path.cwd() / self.thumbnail_path)
|
|
self.browser.find(By.XPATH, Constant.INPUT_FILE_THUMBNAIL).send_keys(
|
|
absolute_thumbnail_path)
|
|
change_display = "document.getElementById('file-loader').style = 'display: block! important'"
|
|
self.browser.driver.execute_script(change_display)
|
|
self.logger.debug(
|
|
'Attached thumbnail {}'.format(self.thumbnail_path))
|
|
|
|
title_field, description_field = self.browser.find_all(By.ID, Constant.TEXTBOX_ID, timeout=15)
|
|
|
|
self.__write_in_field(
|
|
title_field, self.metadata_dict[Constant.VIDEO_TITLE], select_all=True)
|
|
self.logger.debug('The video title was set to \"{}\"'.format(
|
|
self.metadata_dict[Constant.VIDEO_TITLE]))
|
|
|
|
video_description = self.metadata_dict[Constant.VIDEO_DESCRIPTION]
|
|
video_description = video_description.replace("\n", Keys.ENTER)
|
|
if video_description:
|
|
self.__write_in_field(description_field, video_description, select_all=True)
|
|
self.logger.debug('Description filled.')
|
|
|
|
kids_section = self.browser.find(By.NAME, Constant.NOT_MADE_FOR_KIDS_LABEL)
|
|
kids_section.location_once_scrolled_into_view
|
|
time.sleep(random_time())
|
|
|
|
self.browser.find(By.ID, Constant.RADIO_LABEL, kids_section).click()
|
|
self.logger.debug('Selected \"{}\"'.format(Constant.NOT_MADE_FOR_KIDS_LABEL))
|
|
|
|
# Playlist
|
|
playlist = self.metadata_dict[Constant.VIDEO_PLAYLIST]
|
|
if playlist:
|
|
self.browser.find(By.CLASS_NAME, Constant.PL_DROPDOWN_CLASS).click()
|
|
time.sleep(random_time())
|
|
search_field = self.browser.find(By.ID, Constant.PL_SEARCH_INPUT_ID)
|
|
self.__write_in_field(search_field, playlist)
|
|
time.sleep(random_time() * 2)
|
|
playlist_items_container = self.browser.find(By.ID, Constant.PL_ITEMS_CONTAINER_ID)
|
|
# Try to find playlist
|
|
self.logger.debug('Playlist xpath: "{}".'.format(Constant.PL_ITEM_CONTAINER.format(playlist)))
|
|
playlist_item = self.browser.find(By.XPATH, Constant.PL_ITEM_CONTAINER.format(playlist), playlist_items_container)
|
|
if playlist_item:
|
|
self.logger.debug('Playlist found.')
|
|
playlist_item.click()
|
|
time.sleep(random_time())
|
|
else:
|
|
self.logger.debug('Playlist not found. Creating')
|
|
self.__clear_field(search_field)
|
|
time.sleep(random_time())
|
|
|
|
new_playlist_button = self.browser.find(By.CLASS_NAME, Constant.PL_NEW_BUTTON_CLASS)
|
|
new_playlist_button.click()
|
|
|
|
create_playlist_container = self.browser.find(By.ID, Constant.PL_CREATE_PLAYLIST_CONTAINER_ID)
|
|
playlist_title_textbox = self.browser.find(By.XPATH, "//textarea", create_playlist_container)
|
|
self.__write_in_field(playlist_title_textbox, playlist)
|
|
|
|
time.sleep(random_time())
|
|
create_playlist_button = self.browser.find(By.CLASS_NAME, Constant.PL_CREATE_BUTTON_CLASS)
|
|
create_playlist_button.click()
|
|
time.sleep(random_time())
|
|
|
|
done_button = self.browser.find(By.CLASS_NAME, Constant.PL_DONE_BUTTON_CLASS)
|
|
done_button.click()
|
|
|
|
# Advanced options
|
|
self.browser.find(By.ID, Constant.ADVANCED_BUTTON_ID).click()
|
|
self.logger.debug('Clicked MORE OPTIONS')
|
|
time.sleep(random_time())
|
|
|
|
#click not ai (added myself by inspecting element and right clicking copy by xpath)
|
|
not_ai = self.browser.find(By.XPATH, "/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[1]/ytcp-ve/ytcp-video-metadata-editor/div/ytcp-video-metadata-editor-advanced/div[2]/ytkp-altered-content-select/div[2]/tp-yt-paper-radio-group/tp-yt-paper-radio-button[2]/div[1]/div[1]")
|
|
not_ai.click()
|
|
time.sleep(random_time())
|
|
|
|
|
|
# Tags
|
|
tags = self.metadata_dict[Constant.VIDEO_TAGS]
|
|
if tags:
|
|
tags_container = self.browser.find(By.ID, Constant.TAGS_CONTAINER_ID)
|
|
tags_field = self.browser.find(By.ID, Constant.TAGS_INPUT, tags_container)
|
|
self.__write_in_field(tags_field, ','.join(tags))
|
|
self.logger.debug('The tags were set to \"{}\"'.format(tags))
|
|
|
|
|
|
self.browser.find(By.ID, Constant.NEXT_BUTTON).click()
|
|
self.logger.debug('Clicked {} one'.format(Constant.NEXT_BUTTON))
|
|
|
|
self.browser.find(By.ID, Constant.NEXT_BUTTON).click()
|
|
self.logger.debug('Clicked {} two'.format(Constant.NEXT_BUTTON))
|
|
|
|
self.browser.find(By.ID, Constant.NEXT_BUTTON).click()
|
|
self.logger.debug('Clicked {} three'.format(Constant.NEXT_BUTTON))
|
|
|
|
schedule = self.metadata_dict[Constant.VIDEO_SCHEDULE]
|
|
#Schedule
|
|
if schedule:
|
|
upload_time_object = datetime.strptime(schedule, "%m/%d/%Y, %H:%M")
|
|
self.browser.find(By.ID, Constant.SCHEDULE_CONTAINER_ID).click() #click the schedule dropdown
|
|
time.sleep(1) #make sure there is time for the date picker to load
|
|
schedule2 = self.browser.find(By.ID, Constant.SCHEDULE_DATE_ID) #find the date picker
|
|
time.sleep(1) #wait for schedule2 to load
|
|
schedule2.click() #click to open calendar
|
|
time.sleep(1) #wait for the date picker to load
|
|
self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).clear() #clear the date text box
|
|
self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).send_keys(
|
|
datetime.strftime(upload_time_object, "%b %e, %Y"))
|
|
self.browser.find(By.XPATH, Constant.SCHEDULE_DATE_TEXTBOX).send_keys(Keys.ENTER)
|
|
time.sleep(1) #wait for the date picker to load
|
|
self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).click()
|
|
self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).clear()
|
|
time.sleep(1) #wait for the date picker to load
|
|
self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).send_keys(
|
|
datetime.strftime(upload_time_object, "%H:%M"))
|
|
self.browser.find(By.XPATH, Constant.SCHEDULE_TIME).send_keys(Keys.ENTER)
|
|
self.logger.debug(f"Scheduled the video for {schedule}")
|
|
else:
|
|
public_main_button = self.browser.find(By.NAME, Constant.PUBLIC_BUTTON)
|
|
self.browser.find(By.ID, Constant.RADIO_LABEL, public_main_button).click()
|
|
self.logger.debug('Made the video {}'.format(Constant.PUBLIC_BUTTON))
|
|
|
|
video_id = self.__get_video_id()
|
|
|
|
# Check status container and upload progress
|
|
uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER)
|
|
while uploading_status_container is not None:
|
|
uploading_progress = uploading_status_container.get_attribute('value')
|
|
self.logger.debug('Upload video progress: {}%'.format(uploading_progress))
|
|
time.sleep(random_time() * 5)
|
|
uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER)
|
|
|
|
self.logger.debug('Upload container gone.')
|
|
|
|
done_button = self.browser.find(By.ID, Constant.DONE_BUTTON)
|
|
|
|
# Catch such error as
|
|
# "File is a duplicate of a video you have already uploaded"
|
|
if done_button.get_attribute('aria-disabled') == 'true':
|
|
error_message = self.browser.find(By.XPATH, Constant.ERROR_CONTAINER).text
|
|
self.logger.error(error_message)
|
|
return False, None
|
|
|
|
done_button.click()
|
|
self.logger.debug(
|
|
"Published the video with video_id = {}".format(video_id))
|
|
time.sleep(random_time())
|
|
self.browser.get(Constant.YOUTUBE_URL)
|
|
self.__quit()
|
|
return True, video_id
|
|
|
|
def __get_video_id(self) -> Optional[str]:
|
|
video_id = None
|
|
try:
|
|
video_url_container = self.browser.find(
|
|
By.XPATH, Constant.VIDEO_URL_CONTAINER)
|
|
video_url_element = self.browser.find(By.XPATH, Constant.VIDEO_URL_ELEMENT, element=video_url_container)
|
|
video_id = video_url_element.get_attribute(
|
|
Constant.HREF).split('/')[-1]
|
|
except:
|
|
self.logger.warning(Constant.VIDEO_NOT_FOUND_ERROR)
|
|
pass
|
|
return video_id
|
|
|
|
def __quit(self):
|
|
self.browser.driver.quit()
|