From fc445519c0ce1171397e874279590f9e33bf46bd Mon Sep 17 00:00:00 2001 From: offish Date: Sun, 24 Jan 2021 02:11:56 +0100 Subject: [PATCH 1/8] Delete client_secret.json --- twitchtube/client_secret.json | 1 - 1 file changed, 1 deletion(-) delete mode 100644 twitchtube/client_secret.json diff --git a/twitchtube/client_secret.json b/twitchtube/client_secret.json deleted file mode 100644 index 8b13789..0000000 --- a/twitchtube/client_secret.json +++ /dev/null @@ -1 +0,0 @@ - From 18e95e0674a685d0ba4d96b27c72317b27c1f52c Mon Sep 17 00:00:00 2001 From: offish Date: Sun, 24 Jan 2021 02:12:59 +0100 Subject: [PATCH 2/8] Update config.py --- twitchtube/config.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/twitchtube/config.py b/twitchtube/config.py index 60d5ee5..7754780 100644 --- a/twitchtube/config.py +++ b/twitchtube/config.py @@ -11,6 +11,11 @@ # Twitch OAuth Token OAUTH_TOKEN = "" +# Path to the Firefox profile were you are logged into YouTube +ROOT_PROFILE_PATH = "" + +# How many seconds Firefox should sleep for when uploading +SLEEP = 3 # Paths PATH = str(pathlib.Path().absolute()).replace("\\", "/") @@ -93,10 +98,12 @@ # If empty, it would take the title of the first clip, and add "- *category* Highlights Twitch" TITLE = "" -# 20 for Gaming -CATEGORY = 20 +# Category +# Not supported yet +CATEGORY = 20 # 20 for gaming # Tags +# Not supported yet TAGS = { "Just Chatting": "just chatting, just chatting clips, just chatting twitch clips", "Team Fortress 2": "tf2, tf2 twitch, tf2 twitch clips", From df9c3a8171d015957de2920c16b6ae9d67471c39 Mon Sep 17 00:00:00 2001 From: offish Date: Sun, 24 Jan 2021 02:13:17 +0100 Subject: [PATCH 3/8] Update __init__.py --- twitchtube/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitchtube/__init__.py b/twitchtube/__init__.py index a0e5b10..c8946e8 100644 --- a/twitchtube/__init__.py +++ b/twitchtube/__init__.py @@ -1,4 +1,4 @@ __title__ = "twitchtube" __author__ = "offish" __license__ = "MIT" -__version__ = "1.4.1" +__version__ = "1.5.0" From afca40f537aada99bd29fa963c123668c3d0983e Mon Sep 17 00:00:00 2001 From: offish Date: Sun, 24 Jan 2021 02:13:53 +0100 Subject: [PATCH 4/8] Create constants.py --- twitchtube/constants.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 twitchtube/constants.py diff --git a/twitchtube/constants.py b/twitchtube/constants.py new file mode 100644 index 0000000..2119e9f --- /dev/null +++ b/twitchtube/constants.py @@ -0,0 +1,22 @@ +YOUTUBE_URL = "https://www.youtube.com" +YOUTUBE_STUDIO_URL = "https://studio.youtube.com" +YOUTUBE_UPLOAD_URL = "https://www.youtube.com/upload" +TIMEOUT = 3 + +DESCRIPTION_CONTAINER = "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-details/div/ytcp-uploads-basics/ytcp-mention-textbox[2]" +MORE_OPTIONS_CONTAINER = "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[1]/ytcp-uploads-details/div/div/ytcp-button/div" +TEXTBOX = "textbox" +TEXT_INPUT = "text-input" +RADIO_LABEL = "radioLabel" +STATUS_CONTAINER = "/html/body/ytcp-uploads-dialog/paper-dialog/div/ytcp-animatable[2]/div/div[1]/ytcp-video-upload-progress/span" +NOT_MADE_FOR_KIDS_LABEL = "NOT_MADE_FOR_KIDS" +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" +UPLOADED = "uploaded" +ERROR_CONTAINER = '//*[@id="error-message"]' +VIDEO_NOT_FOUND_ERROR = "Could not find video_id" +DONE_BUTTON = "done-button" +INPUT_FILE_VIDEO = "//input[@type='file']" From 675c9db9ce6f3da5f3ea1fff804eb5b45b2d3d7c Mon Sep 17 00:00:00 2001 From: offish Date: Sun, 24 Jan 2021 02:14:33 +0100 Subject: [PATCH 5/8] Update upload.py --- twitchtube/upload.py | 313 +++++++++++-------------------------------- 1 file changed, 81 insertions(+), 232 deletions(-) diff --git a/twitchtube/upload.py b/twitchtube/upload.py index f2d6a0b..d45b9d5 100644 --- a/twitchtube/upload.py +++ b/twitchtube/upload.py @@ -1,265 +1,114 @@ -import http.client as httplib -import json -import os -import random -import time - -import google.oauth2.credentials -import httplib2 -from google_auth_oauthlib.flow import InstalledAppFlow -from googleapiclient.discovery import build -from googleapiclient.errors import HttpError -from googleapiclient.http import MediaFileUpload - -from .config import PATH +from .constants import * from .logging import Log -log = Log() - -# Explicitly tell the underlying HTTP transport library not to retry, since -# we are handling retry logic ourselves. -httplib2.RETRIES = 1 - -# Maximum number of times to retry before giving up. -MAX_RETRIES = 10 - -# Always retry when these exceptions are raised. -RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, httplib.NotConnected, - httplib.IncompleteRead, httplib.ImproperConnectionState, - httplib.CannotSendRequest, httplib.CannotSendHeader, - httplib.ResponseNotReady, httplib.BadStatusLine) - -# Always retry when an apiclient.errors.HttpError with one of these status -# codes is raised. -RETRIABLE_STATUS_CODES = [500, 502, 503, 504] - -# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains -# the OAuth 2.0 information for this application, including its client_id and -# client_secret. You can acquire an OAuth 2.0 client ID and client secret from -# the {{ Google Cloud Console }} at -# {{ https://cloud.google.com/console }}. -# Please ensure that you have enabled the YouTube Data API for your project. -# For more information about using OAuth2 to access the YouTube Data API, see: -# https://developers.google.com/youtube/v3/guides/authentication -# For more information about the client_secrets.json file format, see: -# https://developers.google.com/api-client-library/python/guide/aaa_client_secrets -CLIENT_SECRETS_FILE = f'{PATH}/twitchtube/client_secret.json' - -# This OAuth 2.0 access scope allows an application to upload files to the -# authenticated user's YouTube channel, but doesn't allow other types of access. -SCOPES = ['https://www.googleapis.com/auth/youtube.upload'] -API_SERVICE_NAME = 'youtube' -API_VERSION = 'v3' - -VALID_PRIVACY_STATUSES = ('public', 'private', 'unlisted') - - -# Authorize the request and store authorization credentials. -# Used to generate first auth token. Only needs to happen once. -def getAuthenticatedService(CREDENTIALS_FILE): - flow = InstalledAppFlow.from_client_secrets_file( - CLIENT_SECRETS_FILE, SCOPES, redirect_uri='urn:ietf:wg:oauth:2.0:oob') - auth_url, _ = flow.authorization_url(prompt='consent') - log.error('Please go to this URL: {}'.format(auth_url)) - - code = input('Enter the authorization code: ') - credentials = flow.fetch_token(code=code) - saveCredentials(CREDENTIALS_FILE, credentials) - - return build(API_SERVICE_NAME, API_VERSION, credentials=flow.credentials) +from time import sleep +from pathlib import Path -# Renew credentials after each time being called. +from selenium import webdriver -def getAuthenticatedServiceFromStorage(CREDENTIALS_FILE): - credentials = getCredentialsFromStorage(CREDENTIALS_FILE) - os.remove(CREDENTIALS_FILE) - saveCredentials(CREDENTIALS_FILE, credentials) - return build(API_SERVICE_NAME, API_VERSION, credentials=credentials) - -# Fetch youtube service from saved credentials. +log = Log() -def getCredentialsFromStorage(CREDENTIALS_FILE): - credentials = json.load(open(CREDENTIALS_FILE)) - credentials = json.loads(credentials) +class Upload: + def __init__(self, root_profile_directory: str, meta: dict, timeout: int = TIMEOUT): + self.video = meta.get("file") + self.title = meta.get("title") + self.description = meta.get("description") + self.timeout = timeout - with open(CLIENT_SECRETS_FILE, 'r') as json_file: - client_config = json.load(json_file) + profile = webdriver.FirefoxProfile(root_profile_directory) + options = webdriver.FirefoxOptions() + options.headless = True + self.driver = webdriver.Firefox(firefox_profile=profile, options=options) - if 'access_token' in credentials: - credentials = google.oauth2.credentials.Credentials( - credentials['access_token'], - refresh_token=credentials['refresh_token'], - token_uri=client_config['installed']['token_uri'], - client_id=client_config['installed']['client_id'], - client_secret=client_config['installed']['client_secret']) - else: - credentials = google.oauth2.credentials.Credentials( - credentials['token'], - refresh_token=credentials['_refresh_token'], - token_uri=client_config['installed']['token_uri'], - client_id=client_config['installed']['client_id'], - client_secret=client_config['installed']['client_secret']) + log.info("Firefox is now running") - return credentials + def upload(self) -> (bool, str): -# Store credentials in json file. + self.driver.get(YOUTUBE_UPLOAD_URL) + sleep(self.timeout) + log.info("Trying to upload video to YouTube...") + path = str(Path.cwd() / self.video) + self.driver.find_element_by_xpath(INPUT_FILE_VIDEO).send_keys(path) -def saveCredentials(CREDENTIALS_FILE, credentials): - open(CREDENTIALS_FILE, 'wb') - with open(CREDENTIALS_FILE, 'w') as outfile: - json.dump(json.dumps(credentials, default=lambda o: o.__dict__), outfile) + sleep(self.timeout) + log.info(f"Trying to set {self.title} as title...") + title_field = self.driver.find_element_by_id(TEXTBOX) + title_field.click() + sleep(self.timeout) -def initializeUpload(youtube, title, description, file, category, keywords, privacyStatus): - tags = keywords.split(',') + title_field.clear() + sleep(self.timeout) - body = dict( - snippet=dict( - title=title, - description=description, - tags=tags, - categoryId=category - ), - status=dict( - privacyStatus=privacyStatus - ) - ) + title_field.send_keys(self.title) + sleep(self.timeout) - # Call the API's videos.insert method to create and upload the video. - insert_request = youtube.videos().insert( - part=','.join(body.keys()), - body=body, - # The chunksize parameter specifies the size of each chunk of data, in - # bytes, that will be uploaded at a time. Set a higher value for - # reliable connections as fewer chunks lead to faster uploads. Set a lower - # value for better recovery on less reliable connections. - # - # Setting 'chunksize' equal to -1 in the code below means that the entire - # file will be uploaded in a single HTTP request. (If the upload fails, - # it will still be retried where it left off.) This is usually a best - # practice, but if you're using Python older than 2.6 or if you're - # running on App Engine, you should set the chunksize to something like - # 1024 * 1024 (1 megabyte). - media_body=MediaFileUpload(file, chunksize=-1, resumable=True) - ) + description = self.description + if description: + log.info(f"Trying to set {self.description} as description...") + container = self.driver.find_element_by_xpath(DESCRIPTION_CONTAINER) + description_field = container.find_element_by_id(TEXTBOX) + description_field.click() + sleep(self.timeout) - return resumableUpload(insert_request) + description_field.clear() + sleep(self.timeout) + description_field.send_keys(self.description) -def thumbnails_set(client, media_file, **kwargs): - request = client.thumbnails().set(media_body=MediaFileUpload( - media_file, chunksize=-1, resumable=True), **kwargs) + log.info("Trying to set video to 'Not made for kids'...") + kids_section = self.driver.find_element_by_name(NOT_MADE_FOR_KIDS_LABEL) + kids_section.find_element_by_id(RADIO_LABEL).click() + sleep(self.timeout) - # See full sample for function - return resumable_upload_thumbnails(request) + self.driver.find_element_by_id(NEXT_BUTTON).click() + sleep(self.timeout) -# This method implements an exponential backoff strategy to resume a -# failed upload. + self.driver.find_element_by_id(NEXT_BUTTON).click() + sleep(self.timeout) + log.info("Trying to set video visibility to public...") + public_main_button = self.driver.find_element_by_name(PUBLIC_BUTTON) + public_main_button.find_element_by_id(RADIO_LABEL).click() + video_id = self.get_video_id() -def resumableUpload(request): - response = None - error = None - retry = 0 - while response is None: - try: - log.info('Uploading file...') - status, response = request.next_chunk() - if response is not None: - if 'id' in response: - log.info('Video id "%s" was successfully uploaded.\n' % - response['id']) - return response['id'] - else: - exit('The upload failed with an unexpected response: %s' % response) - except HttpError as e: - if e.resp.status in RETRIABLE_STATUS_CODES: - error = 'A retriable HTTP error %d occurred:\n%s' % ( - e.resp.status, e.content) + status_container = self.driver.find_element_by_xpath(STATUS_CONTAINER) + while True: + in_process = status_container.text.find(UPLOADED) != -1 + if in_process: + sleep(self.timeout) else: - raise - except(RETRIABLE_EXCEPTIONS, e): - error = 'A retriable error occurred: %s' % e - - if error is not None: - log.error(error) - retry += 1 - if retry > MAX_RETRIES: - exit('No longer attempting to retry.') + break - max_sleep = 2 ** retry - sleep_seconds = random.random() * max_sleep - log.warn('Sleeping %f seconds and then retrying...' % sleep_seconds) - time.sleep(sleep_seconds) + done_button = self.driver.find_element_by_id(DONE_BUTTON) + if done_button.get_attribute("aria-disabled") == "true": + error_message = self.driver.find_element_by_xpath(ERROR_CONTAINER).text + return False, None -def upload_video_to_youtube(config: dict): - CREDENTIALS_FILE = f'{PATH}/credentials/credentials.json' + done_button.click() + sleep(self.timeout) + self.driver.get(YOUTUBE_URL) + self.close() + return True, video_id - if os.path.isfile(CREDENTIALS_FILE): - youtube = getAuthenticatedServiceFromStorage(CREDENTIALS_FILE) - else: + def get_video_id(self) -> str: + video_id = None try: - os.makedirs(f'{PATH}/credentials') - except FileExistsError: - pass - youtube = getAuthenticatedService(CREDENTIALS_FILE) - - try: - # Upload video - - videoId = initializeUpload( - youtube, - title=config['title'], - description=config['description'], - file=config['file'], - category=config['category'], - keywords=config['keywords'], - privacyStatus='private' - ) + video_url_container = self.driver.find_element_by_xpath(VIDEO_URL_CONTAINER) + video_url_element = video_url_container.find_element_by_xpath( + VIDEO_URL_ELEMENT + ) - # Upload thumbnail - if 'thumbnail' in config: - thumbnails_set(youtube, config['thumbnail'], videoId=videoId) - except HttpError as e: - log.error('An HTTP error %d occurred:\n%s' % (e.resp.status, e.content)) - - -def resumable_upload_thumbnails(request, method='insert'): - response = None - error = None - retry = 0 - while response is None: - try: - log.info('Uploading thumbnail...') - status, response = request.next_chunk() - if response is not None: - if method == 'insert' and 'id' in response: - log.info(response) - elif method != 'insert' or 'id' not in response: - log.info(response) - else: - exit("The upload failed with an unexpected response: %s" % response) - except HttpError as e: - if e.resp.status in RETRIABLE_STATUS_CODES: - error = "A retriable HTTP error %d occurred:\n%s" % ( - e.resp.status, e.content) - else: - raise - except RETRIABLE_EXCEPTIONS as e: - error = "A retriable error occurred: %s" % e - - if error is not None: - log.error(error) - retry += 1 - if retry > MAX_RETRIES: - exit("No longer attempting to retry.") + video_id = video_url_element.get_attribute(HREF).split("/")[-1] + except: + pass + return video_id - max_sleep = 2 ** retry - sleep_seconds = random.random() * max_sleep - log.warn('Sleeping %f seconds and then retrying...' % sleep_seconds) - time.sleep(sleep_seconds) + def close(self): + self.driver.quit() + log.info("Closed Firefox") From a7c4d5f229e4a8c2a87d66c947edc12fec27f111 Mon Sep 17 00:00:00 2001 From: offish Date: Sun, 24 Jan 2021 02:15:19 +0100 Subject: [PATCH 6/8] Update main.py --- main.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 8cf0dfe..e62bdc8 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ from twitchtube.logging import Log from twitchtube.config import * -from twitchtube.upload import upload_video_to_youtube +from twitchtube.upload import Upload from twitchtube.utils import create_video_config, get_date from twitchtube.clips import get_clips, download_clips from twitchtube.video import render @@ -54,10 +54,13 @@ dump(config, f, indent=4) if UPLOAD_TO_YOUTUBE and RENDER_VIDEO: - try: - upload_video_to_youtube(config) - except JSONDecodeError: - log.error("Your client_secret is empty or has wrong syntax") + upload = Upload(ROOT_PROFILE_PATH, config, SLEEP) + was_uploaded, video_id = upload.upload() + + if was_uploaded: + log.info(f"{video_id} was successfully uploaded to YouTube") + else: + log.error("Video was not successfully uploaded to YouTube") if DELETE_CLIPS: # Get all the mp4 files in the path and delte them From d805d72dd8e8b268e6113976a979bcbedf936821 Mon Sep 17 00:00:00 2001 From: offish Date: Sun, 24 Jan 2021 02:16:07 +0100 Subject: [PATCH 7/8] Update README.md --- README.md | 80 ++++++++++++++++++++----------------------------------- 1 file changed, 29 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 38daf8b..683e22c 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ Automatically make video compilations of the most viewed Twitch clips and upload ## Features * Downloads the most popular clips from given `channel` or `game` * Downloads only the needed clips to reach `VIDEO_LENGTH` +* Upload automatically to YouTube using Selenium and Firefox +* Customizable * Option for concatninating clips into one video * Option for custom intro, transition and outro * Option for custom resolution @@ -32,13 +34,13 @@ this repo. Only a couple of titles and thumbnails have been changed. ## Installation Download the repo as ZIP and unzip it somewhere accessible. -To install all the packages needed you need to run this command (has to be in -the right directory). +To install all the packages needed, you have to run this command. Has to be in same directory as the `requirements.txt` and `main.py` files are. ``` pip install -r requirements.txt ``` +Download [geckodriver](https://github.com/mozilla/geckodriver/releases) and place the .exe file inside `C:\Users\USERNAME\AppData\Local\Programs\Python\Python37`. ## Configuration ### Creating your Twitch Application @@ -46,90 +48,66 @@ Go to https://dev.twitch.tv/console and register a new application. The name of the application does not matter. Set "OAuth Redirect URLs" to https://twitchapps.com/tokengen/ Set category to "Application Integration" or "Other". You will now see your Client ID, copy this ID. -Go to [`config.py`](twitchtube/config.py), find CLIENT_ID and paste it inside apostrophes. +Go to [`config.py`](twitchtube/config.py), find `CLIENT_ID` and paste it inside apostrophes. ### Getting your OAuth Token Now head over to https://twitchapps.com/tokengen/ and paste in your Client ID. Scopes does not matter in our case. Click "Connect" and then authorize with Twitch. Copy your OAuth Token, go to [`config.py`](twitchtube/config.py), find `OAUTH_TOKEN` and paste it inside the apostrophes. -### Creating your Google Project -Go to https://console.cloud.google.com/ and create a new project. -Name does not matter. - -### Enabling YouTube Data API v3 -Click on the menu on the left side of your screen and navigate to "APIs & Services". -Hover over this button and click "Library". -Search for "YouTube Data API v3" and click the first result. -Enable this API. - -### Getting your client_secret -When you have clicked "Enable" you should now be under the "Overview" tab. -Click "Credentials" and then "+ Create Credentials". -You will now see 3 options, click "OAuth client ID". -Now you might need to configure consent screen. -If you need to configure this, click "External" and then "Create". -Write something in the application name field, might be wise to name it something you will remember like "twitchtube" or -"YouTube Twitch Bot". -Now you will see your application, go to "Credentials" again and click "+ Create Credentials" and then "OAuth client ID". -Set application type to Desktop app and name it whatever. -Click "Ok", and then click the download icon. -Open the JSON file that gets downloaded, select everything in this fiel and paste it into the [`client_secret.json`](twitchtube/client_secret.json) file. - -### Adding/removing to LIST -If you want to add a game or channel, you simply write the name of the game how it appears on Twitch inside the `LIST` list in [`config.py`](twitchtube/config.py). -If you want to add Rust for example, `LIST` should look like this: +### Setting up Firefox +Open Firefox and create a new profile for Selenium, (this is not needed, but highly recommended). Go to `about:profiles` and click "Create a New profile", name it "Selenium" or whatever. When you have done that, copy the "Root Directory" path of that profile and paste it into the `ROOT_PROFILE_PATH` in [`config.py`](twitchtube/config.py). Now click "Launch profile in new browser". Go to [YouTube](https://youtube.com) and login to the account you want to use with twitchtube. VoilĂ , you are now set. You migth want to set another profile to your default profile though. + +### Adding and removing games or channels to LIST +**`LIST` MUST MATCH `MODE`. IF `MODE` IS SET TO `GAME`, THERE SHOULD ONLY BE GAMES INSIDE OF `LIST`, SAME GOES FOR `CHANNEL`.** + +If you want to add a game or channel, you simply write the name of the game or channel, how it appears on Twitch, inside the `LIST` in [`config.py`](twitchtube/config.py). +If you want to add Rust for example, then `LIST` should look like this: ```python -LIST = ['Rust', 'Just Chatting', 'Team Fortress 2'] +LIST = ["Rust", "Just Chatting", "Team Fortress 2"] ``` Last entry in the list should not have a comma. -If you only want to have 1 game or channel, `LIST` should look like this: +If you only want to have one game or channel, `LIST` should look like this: ```python -LIST = ['Just Chatting'] +LIST = ["Just Chatting"] ``` Example: ```python -LIST = ['Just Chatting'] +MODE = "game" + +LIST = ["Just Chatting"] +TITLE = "Most Viewed Just Chatting Clips - 24.01.2021" + +# Tags are currently not supported TAGS = { - 'Just Chatting': 'just chatting, just chatting twitch, just chatting twitch highlights' + "Just Chatting": "just chatting, just chatting twitch, just chatting twitch highlights" } +# Descriptions are though DESCRIPTIONS = { - 'Just Chatting': 'The most viewed Just Chatting clips today.\n\n{}\n#Twitch #TwitchHighlights #Just Chatting' + "Just Chatting": "The most viewed Just Chatting clips today.\n\n{}\n #Twitch #TwitchHighlights #Just Chatting" } ``` -Counter-Strike: Global Offensive is currently not supported since folders can't include colons in their folder name. - -## Explanation -The script starts off by checking every game or channel listed in the config. It will then create a folder with -the current date as folder name and inside this folder, it will create another folder for the -with the current game or channel as folder name. It will send a request to Twitch's Kraken API -and ask for the top 100 clips. It will then save this data in a JSON -file called `clips.json`. It will loop through the clip URLs and download each clip -till it reaches the limit specifed in the config. When the limit is reached, which means the video is -long enough it will take all the mp4 files in the game or channel folder and concatenete these clips into one -video (if specified). If time limit given is too big, it will just continue anyways. When the video is -done rendering, it will upload it to YouTube (if specified). When the video is uploaded it will delete -the clips (if specified) and create a new folder for the next game or channel in `LIST` (if any) and -redo the process written above. +Counter-Strike: Global Offensive is currently not supported since folders cannot include colons in their folder name. + ## Running -To run the script run this command (must be in the correct folder). +To run the bot, use this command. Has to be in same directory as the `requirements.txt` and `main.py` files are. ``` python main.py ``` ## Note -I've only tested this script using Python 3.7.3, but should work with later versions. +I have only tested this bot using Windows 10 and Python 3.7.3, but should work on other operating systems, and Python 3 versions. ## License MIT License From 590b4af184ed2db33ddfec97e6f855d45c81bb39 Mon Sep 17 00:00:00 2001 From: offish Date: Sun, 24 Jan 2021 02:16:25 +0100 Subject: [PATCH 8/8] Update requirements.txt --- requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index dedeb8f..99c78fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,3 @@ moviepy==1.0.3 colorama -httplib2 -google-auth-oauthlib -google-api-python-client -google-auth +selenium