feat: implement vertical crop, local LLM deepseek-v4-flash, multi-threading, word-level pink box highlight subtitles, and Nextcloud scan sync
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
class Constant:
|
||||
"""A class for storing constants for YoutubeUploader class"""
|
||||
YOUTUBE_URL = 'https://www.youtube.com'
|
||||
YOUTUBE_STUDIO_URL = 'https://studio.youtube.com'
|
||||
YOUTUBE_UPLOAD_URL = 'https://www.youtube.com/upload'
|
||||
USER_WAITING_TIME = 1
|
||||
VIDEO_TITLE = 'title'
|
||||
VIDEO_DESCRIPTION = 'description'
|
||||
VIDEO_EDIT = 'edit'
|
||||
VIDEO_TAGS = 'tags'
|
||||
TEXTBOX_ID = 'textbox'
|
||||
TEXT_INPUT = 'text-input'
|
||||
RADIO_LABEL = 'radioLabel'
|
||||
UPLOADING_STATUS_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[2]/div/div[1]/ytcp-video-upload-progress[@uploading=""]'
|
||||
NOT_MADE_FOR_KIDS_LABEL = 'VIDEO_MADE_FOR_KIDS_NOT_MFK'
|
||||
|
||||
|
||||
|
||||
Click_Add = '//*[@id="mount_0_0_7N"]/div/div/div[2]/div/div/div[1]/div[1]/div[2]/div/div/div/div/div[2]/div[7]/div/span/div/a/div/div[1]/div/div/svg'
|
||||
|
||||
UPLOAD_DIALOG = '//ytcp-uploads-dialog'
|
||||
ADVANCED_BUTTON_ID = 'toggle-button'
|
||||
TAGS_CONTAINER_ID = 'tags-container'
|
||||
|
||||
TAGS_INPUT = 'text-input'
|
||||
NEXT_BUTTON = 'next-button'
|
||||
PUBLIC_BUTTON = 'PUBLIC'
|
||||
VIDEO_URL_CONTAINER = "//span[@class='video-url-fadeable style-scope ytcp-video-info']"
|
||||
VIDEO_URL_ELEMENT = "//a[@class='style-scope ytcp-video-info']"
|
||||
HREF = 'href'
|
||||
ERROR_CONTAINER = '//*[@id="error-message"]'
|
||||
VIDEO_NOT_FOUND_ERROR = 'Could not find video_id'
|
||||
DONE_BUTTON = 'done-button'
|
||||
INPUT_FILE_VIDEO = "//input[@type='file']"
|
||||
INPUT_FILE_THUMBNAIL = "//input[@id='file-loader']"
|
||||
|
||||
# Playlist
|
||||
VIDEO_PLAYLIST = 'playlist_title'
|
||||
PL_DROPDOWN_CLASS = 'ytcp-video-metadata-playlists'
|
||||
PL_SEARCH_INPUT_ID = 'search-input'
|
||||
PL_ITEMS_CONTAINER_ID = 'items'
|
||||
PL_ITEM_CONTAINER = '//span[text()="{}"]'
|
||||
PL_NEW_BUTTON_CLASS = 'new-playlist-button'
|
||||
PL_CREATE_PLAYLIST_CONTAINER_ID = 'create-playlist-form'
|
||||
PL_CREATE_BUTTON_CLASS = 'create-playlist-button'
|
||||
PL_DONE_BUTTON_CLASS = 'done-button'
|
||||
|
||||
#Schedule
|
||||
VIDEO_SCHEDULE = 'schedule'
|
||||
SCHEDULE_CONTAINER_ID = 'second-container-expand-button'
|
||||
SCHEDULE_DATE_ID = 'datepicker-trigger'
|
||||
SCHEDULE_DATE_TEXTBOX = '/html/body/ytcp-date-picker/tp-yt-paper-dialog/div/form/tp-yt-paper-input/tp-yt-paper-input-container/div[2]/div/iron-input/input'
|
||||
SCHEDULE_TIME = '//*[@id="input-1"]/input'
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
"""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 InstagramUploader:
|
||||
"""A class for uploading videos on Instagram 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 instagram =" +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("https://www.instagram.com/")
|
||||
time.sleep(1)
|
||||
|
||||
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(1)
|
||||
self.browser.refresh()
|
||||
else:
|
||||
self.logger.info('Please sign in and then press enter')
|
||||
input()
|
||||
self.browser.get("https://www.instagram.com/")
|
||||
time.sleep(3)
|
||||
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(3)
|
||||
if self.is_mac:
|
||||
field.send_keys(Keys.COMMAND + 'a')
|
||||
else:
|
||||
field.send_keys(Keys.CONTROL + 'a')
|
||||
time.sleep(2)
|
||||
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)
|
||||
|
||||
#find element, if not found, wait 3 seconds and try again
|
||||
#by - the type of element to find
|
||||
#value - the value of the element to find
|
||||
#name - the name of the element to find
|
||||
#return - the element found
|
||||
def find_element(self, by, value: str, name: str):
|
||||
element = None
|
||||
startTime = time.time()
|
||||
while element is None:
|
||||
element = self.browser.find(by, value)
|
||||
time.sleep(3)
|
||||
print(f"{name} not found")
|
||||
#if 1 minute has passed, break the loop
|
||||
if time.time() - startTime > 60:
|
||||
break
|
||||
|
||||
print(f"{name} found")
|
||||
return element
|
||||
|
||||
def __upload(self) -> bool:
|
||||
self.browser.get("https://www.instagram.com/?next=%2F")
|
||||
uploading_status_container = InstagramUploader.find_element(self,By.CSS_SELECTOR, 'button._a9--:nth-child(2)', "uploading status container")
|
||||
uploading_status_container.click()
|
||||
print("clicked no updates")
|
||||
time.sleep(2)
|
||||
upload_button = None
|
||||
upload_button = InstagramUploader.find_element(self,By.XPATH, '/html/body/div[1]/div/div/div[2]/div/div/div[1]/div[1]/div[2]/div/div/div/div/div[2]/div[7]/div/span/div/a', "upload button")
|
||||
upload_button.click()
|
||||
print("clicked create")
|
||||
time.sleep(3)
|
||||
self.browser.find(By.XPATH, '/html/body/div[1]/div/div/div[2]/div/div/div[1]/div[1]/div[2]/div/div/div/div/div[2]/div[7]/div/span/div/div/div/div[1]/a[1]').click()
|
||||
print("clicked post")
|
||||
time.sleep(3)
|
||||
|
||||
absolute_video_path = str(Path.cwd() / self.video_path)
|
||||
videoUpload = InstagramUploader.find_element(self,By.CSS_SELECTOR,'._ac69', "video upload")
|
||||
videoUpload.send_keys(absolute_video_path)
|
||||
print("gave video path: ", absolute_video_path)
|
||||
|
||||
|
||||
time.sleep(3)
|
||||
next = InstagramUploader.find_element(self,By.CSS_SELECTOR, '._acap', "next")
|
||||
next.click()
|
||||
print("clicked next")
|
||||
time.sleep(3)
|
||||
size = InstagramUploader.find_element(self,By.CSS_SELECTOR, 'div.xnz67gz > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(2) > div:nth-child(1) > button:nth-child(1)', "size")
|
||||
size.click()
|
||||
print("clicked size")
|
||||
time.sleep(3)
|
||||
phone_resolution = InstagramUploader.find_element(self,By.CSS_SELECTOR, 'div.x1i10hfl:nth-child(5)', "phone resolution")
|
||||
phone_resolution.click()
|
||||
print("clicked phone resolution")
|
||||
|
||||
clickOff = InstagramUploader.find_element(self,By.CSS_SELECTOR, 'div.xnz67gz > div:nth-child(1) > div:nth-child(3)', "click off")
|
||||
clickOff.click()
|
||||
time.sleep(3)
|
||||
next = InstagramUploader.find_element(self,By.CSS_SELECTOR, '.x1f6kntn', "next")
|
||||
next.click()
|
||||
print("clicked next")
|
||||
time.sleep(3)
|
||||
next = InstagramUploader.find_element(self,By.CSS_SELECTOR, '.xyamay9 > div:nth-child(1)', "next")
|
||||
next.click()
|
||||
print("clicked next")
|
||||
time.sleep(3)
|
||||
|
||||
description_field = self.browser.find(By.XPATH, '/html/body/div[6]/div[1]/div/div[3]/div/div/div/div/div/div/div/div[2]/div[2]/div/div/div/div[1]/div[2]/div/div[1]/div[1]')
|
||||
|
||||
video_description :str = self.metadata_dict[Constant.VIDEO_DESCRIPTION]
|
||||
video_description = video_description.replace("\n", Keys.ENTER)
|
||||
if video_description:
|
||||
description_field = self.browser.find(By.CSS_SELECTOR, '.x1hnll1o')
|
||||
print("Video Description: ", video_description)
|
||||
description_field.click()
|
||||
time.sleep(2)
|
||||
[description_field.send_keys(c) for c in video_description] #send_keys(video_description)
|
||||
print("Video description added")
|
||||
time.sleep(2)
|
||||
share = InstagramUploader.find_element(self,By.CSS_SELECTOR, '.x1f6kntn', "share")
|
||||
share.click()
|
||||
print("clicked share")
|
||||
|
||||
#uploading_status_container = self.browser.find(By.XPATH, Constant.UPLOADING_STATUS_CONTAINER)
|
||||
uploading_status_container_done = InstagramUploader.find_element(self,By.CSS_SELECTOR, 'div.x5yr21d:nth-child(1) > div:nth-child(1) > div:nth-child(2)', "uploading status container done")
|
||||
|
||||
self.__quit()
|
||||
return True
|
||||
|
||||
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,23 @@
|
||||
import os
|
||||
from .youtube_uploader_selenium import YouTubeUploader
|
||||
from .Instagram_Uploader.instagramUploader import InstagramUploader
|
||||
from termcolor import colored
|
||||
|
||||
def login_youtube(metadata:str) -> None:
|
||||
print(colored("Logging in to Youtube", "blue"))
|
||||
uploader = YouTubeUploader("dummy", metadata, headless= False)
|
||||
uploader.login()
|
||||
|
||||
def login_instagram(metadata:str) -> None:
|
||||
print(colored("Logging in to Instagram", "magenta"))
|
||||
uploader = InstagramUploader("dummy",metadata, headless= False)
|
||||
uploader.login()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(colored("Logging in to Youtube and Instagram to save cookies for future use", "green"))
|
||||
print(colored("Firefox browser required", "yellow"))
|
||||
outputDirectory :str = os.path.join(os.getcwd(), "done_split")
|
||||
metadata_path :str = os.path.join(outputDirectory, "upload.json")
|
||||
login_youtube(metadata_path)
|
||||
login_instagram(metadata_path)
|
||||
print(colored("Cookies saved successfully. Now you can upload videos without logging in again", "cyan"))
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
from tiktok_uploader.upload import upload_video, upload_videos
|
||||
from tiktok_uploader.auth import AuthBackend
|
||||
|
||||
|
||||
#https://github.com/wkaisertexas/tiktok-uploader?tab=readme-ov-file
|
||||
def upload_video_Tiktok(video_path :str, description:str, cookies:str) -> None:
|
||||
|
||||
username = 'your username here'
|
||||
password = 'password here'
|
||||
|
||||
upload_video(filename=video_path, description=description, cookies= cookies, username=username, password=password)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
from .youtube_uploader_selenium import YouTubeUploader
|
||||
#from https://github.com/linouk23/youtube_uploader_selenium
|
||||
from .Instagram_Uploader.instagramUploader import InstagramUploader
|
||||
|
||||
def upload_video_youtube(video_path: str, metadata_path: str, headless: bool, i : int = 0) -> None:
|
||||
uploader = YouTubeUploader(video_path, metadata_path, headless=headless)
|
||||
try:
|
||||
uploader.upload()
|
||||
except Exception as e:
|
||||
print(f"Error uploading video: {e}")
|
||||
print("Retrying upload...")
|
||||
if i < 5:
|
||||
print(f"Retry number {i}")
|
||||
i += 1
|
||||
upload_video_youtube(video_path, metadata_path, headless, i) # retry the upload recursively
|
||||
|
||||
|
||||
def upload_video_Instagram(video_path: str, metadata_path: str, headless: bool, i: int = 0) -> None:
|
||||
uploader = InstagramUploader(video_path, metadata_path, headless=headless)
|
||||
try:
|
||||
uploader.upload()
|
||||
except Exception as e:
|
||||
print(f"Error uploading video: {e}")
|
||||
print("Retrying upload...")
|
||||
|
||||
if(i < 5):
|
||||
print(f"Retry number {i}")
|
||||
i += 1
|
||||
upload_video_Instagram(video_path, metadata_path,headless, i) # retry the upload recursively
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
class Constant:
|
||||
"""A class for storing constants for YoutubeUploader class"""
|
||||
YOUTUBE_URL = 'https://www.youtube.com'
|
||||
YOUTUBE_STUDIO_URL = 'https://studio.youtube.com'
|
||||
YOUTUBE_UPLOAD_URL = 'https://www.youtube.com/upload'
|
||||
USER_WAITING_TIME = 1
|
||||
VIDEO_TITLE = 'title'
|
||||
VIDEO_DESCRIPTION = 'description'
|
||||
VIDEO_EDIT = 'edit'
|
||||
VIDEO_TAGS = 'tags'
|
||||
TEXTBOX_ID = 'textbox'
|
||||
TEXT_INPUT = 'text-input'
|
||||
RADIO_LABEL = 'radioLabel'
|
||||
UPLOADING_STATUS_CONTAINER = '/html/body/ytcp-uploads-dialog/tp-yt-paper-dialog/div/ytcp-animatable[2]/div/div[1]/ytcp-video-upload-progress[@uploading=""]'
|
||||
NOT_MADE_FOR_KIDS_LABEL = 'VIDEO_MADE_FOR_KIDS_NOT_MFK'
|
||||
|
||||
UPLOAD_DIALOG = '//ytcp-uploads-dialog'
|
||||
ADVANCED_BUTTON_ID = 'toggle-button'
|
||||
TAGS_CONTAINER_ID = 'tags-container'
|
||||
|
||||
TAGS_INPUT = 'text-input'
|
||||
NEXT_BUTTON = 'next-button'
|
||||
PUBLIC_BUTTON = 'PUBLIC'
|
||||
VIDEO_URL_CONTAINER = "//span[@class='video-url-fadeable style-scope ytcp-video-info']"
|
||||
VIDEO_URL_ELEMENT = "//a[@class='style-scope ytcp-video-info']"
|
||||
HREF = 'href'
|
||||
ERROR_CONTAINER = '//*[@id="error-message"]'
|
||||
VIDEO_NOT_FOUND_ERROR = 'Could not find video_id'
|
||||
DONE_BUTTON = 'done-button'
|
||||
INPUT_FILE_VIDEO = "//input[@type='file']"
|
||||
INPUT_FILE_THUMBNAIL = "//input[@id='file-loader']"
|
||||
|
||||
# Playlist
|
||||
VIDEO_PLAYLIST = 'playlist_title'
|
||||
PL_DROPDOWN_CLASS = 'ytcp-video-metadata-playlists'
|
||||
PL_SEARCH_INPUT_ID = 'search-input'
|
||||
PL_ITEMS_CONTAINER_ID = 'items'
|
||||
PL_ITEM_CONTAINER = '//span[text()="{}"]'
|
||||
PL_NEW_BUTTON_CLASS = 'new-playlist-button'
|
||||
PL_CREATE_PLAYLIST_CONTAINER_ID = 'create-playlist-form'
|
||||
PL_CREATE_BUTTON_CLASS = 'create-playlist-button'
|
||||
PL_DONE_BUTTON_CLASS = 'done-button'
|
||||
|
||||
#Schedule
|
||||
VIDEO_SCHEDULE = 'schedule'
|
||||
SCHEDULE_CONTAINER_ID = 'second-container-expand-button'
|
||||
SCHEDULE_DATE_ID = 'datepicker-trigger'
|
||||
SCHEDULE_DATE_TEXTBOX = '/html/body/ytcp-date-picker/tp-yt-paper-dialog/div/form/tp-yt-paper-input/tp-yt-paper-input-container/div[2]/div/iron-input/input'
|
||||
SCHEDULE_TIME = '//*[@id="input-1"]/input'
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user