diff --git a/.gitignore b/.gitignore index 3da725f..3ab4df0 100644 --- a/.gitignore +++ b/.gitignore @@ -235,7 +235,7 @@ assets/ out .DS_Store .setup-done-before -results/* +results/*.mp4 reddit-bot-351418-5560ebc49cac.json /.idea *.pyc @@ -244,3 +244,6 @@ video_creation/data/envvars.txt config.toml video_creation/data/videos.json +results/client_secrets.json +results/client_secrets.json.template +results/upload_video.py-oauth2.json diff --git a/results/README.md b/results/README.md new file mode 100644 index 0000000..059ddf5 --- /dev/null +++ b/results/README.md @@ -0,0 +1,44 @@ +# Upload script + +- videos go into this results folder +- script needs to be in the same directory as videos +- videos scheduled to release every 4 hours + +# Files needed + +- client_secrets.json + +I have left a .template for you to copy + +``` +{ + "web": { + "client_id": < your client id goes here >, + "client_secret": < your client secret goes here >, + "redirect_uris": [], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token" + } +} + + +``` + +# Changes required + +- All objects in videos.json + +``` +{ + ..., + "uploaded": false, + "uploaded_at": "2022-10-20 12:00:00" +} +``` + +YYYY-MM-DD HH:MM:SS + +you can get the uploaded_at time by running the first_time.py script and using the print output. +uploaded : false has been added to the videos.py payload so should be uploaded : false by defualt. + +[Link to the docs](https://developers.google.com/youtube/v3/guides/uploading_a_video) I used to get this working. diff --git a/results/first_time.py b/results/first_time.py new file mode 100644 index 0000000..b37c1d7 --- /dev/null +++ b/results/first_time.py @@ -0,0 +1,5 @@ +import datetime + +format_data = "%Y-%m-%d %H:%M:%S.%f" +time = datetime.datetime.now().strftime(format_data) +print(time) diff --git a/results/upload_video.py b/results/upload_video.py new file mode 100644 index 0000000..68d1bb3 --- /dev/null +++ b/results/upload_video.py @@ -0,0 +1,249 @@ +import httplib2 +import os +import random +import sys +import time +import datetime +import json + +from apiclient.discovery import build +from apiclient.errors import HttpError +from apiclient.http import MediaFileUpload +from oauth2client.client import flow_from_clientsecrets +from oauth2client.file import Storage +from oauth2client.tools import argparser, run_flow +from datetime import timedelta +from datetime import datetime + +# 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, +) + +# 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 API Console at +# https://console.developers.google.com/. +# 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 = "client_secrets.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. +YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload" +YOUTUBE_API_SERVICE_NAME = "youtube" +YOUTUBE_API_VERSION = "v3" + +# This variable defines a message to display if the CLIENT_SECRETS_FILE is +# missing. +MISSING_CLIENT_SECRETS_MESSAGE = """ +WARNING: Please configure OAuth 2.0 + +To make this sample run you will need to populate the client_secrets.json file +found at: + + %s + +with information from the API Console +https://console.developers.google.com/ + +For more information about the client_secrets.json file format, please visit: +https://developers.google.com/api-client-library/python/guide/aaa_client_secrets +""" % os.path.abspath( + os.path.join(os.path.dirname(__file__), CLIENT_SECRETS_FILE) +) + +VALID_PRIVACY_STATUSES = ("public", "private", "unlisted") + + +def get_authenticated_service(args): + # print(CLIENT_SECRETS_FILE) + flow = flow_from_clientsecrets( + CLIENT_SECRETS_FILE, + scope=YOUTUBE_UPLOAD_SCOPE, + message=MISSING_CLIENT_SECRETS_MESSAGE, + ) + + storage = Storage("%s-oauth2.json" % sys.argv[0]) + credentials = storage.get() + + if credentials is None or credentials.invalid: + credentials = run_flow(flow, storage, args) + + return build( + YOUTUBE_API_SERVICE_NAME, + YOUTUBE_API_VERSION, + http=credentials.authorize(httplib2.Http()), + ) + + +def initialize_upload(youtube, options, i, next_upload_time): + tags = None + if options["keywords"]: + tags = options["keywords"].split(",") + + body = dict( + snippet=dict( + title=options["title"], + description=options["description"], + tags=tags, + categoryId=options["category"], + ), + status=dict( + privacyStatus=options["privacyStatus"], + publishAt=options[ + "publishTime" + ], # might need to be publishedAt, implement this + ), + ) + + # Call the API's videos.insert method to create and upload the video. + insert_request = youtube.videos().insert( + part=",".join(body.keys()), + body=body, + media_body=MediaFileUpload(options["file"], chunksize=-1, resumable=True), + ) + + resumable_upload(insert_request, i, next_upload_time) + + +# This method implements an exponential backoff strategy to resume a +# failed upload. +def resumable_upload(insert_request, i, next_upload_time): + response = None + error = None + retry = 0 + while response is None: + try: + print("Uploading file...") + status, response = insert_request.next_chunk() + if response is not None: + if "id" in response: + print("Video id '%s' was successfully uploaded." % response["id"]) + ## upload success + updateUploadedStatus(i, next_upload_time) + + 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: + print(error) + retry += 1 + if retry > MAX_RETRIES: + exit("No longer attempting to retry.") + + max_sleep = 2**retry + sleep_seconds = random.random() * max_sleep + print("Sleeping %f seconds and then retrying...") % sleep_seconds + time.sleep(sleep_seconds) + + +def updateUploadedStatus(currIndex, time): + # opens the json containing video_creation data (used for logic and upload data) + f = open("..\\video_creation\\data\\videos.json") + # loads videos.json into a dictionary + video_data = json.load(f) + + video_data[currIndex]["uploaded_at"] = time + video_data[currIndex]["uploaded"] = True + + # serialises the dictionary to json + json_obj = json.dumps(video_data, indent=4, default=str) + # writes the file + with open("..\\video_creation\\data\\videos.json", "w") as outfile: + outfile.write(json_obj) + + # print(datetime.datetime.now().isoformat()) + # print(video_data[i - 1]) + # print(video_data[i]) + + +if __name__ == "__main__": + + # opens the json containing video_creation data (used for logic and upload data) + f = open("..\\video_creation\\data\\videos.json") + # loads videos.json into a dictionary + video_data = json.load(f) + + descriptionBeginning = "r/askReddit | " + descriptionEnd = " 🔔 Hit the bell next to Subscribe so you never miss a video! ❤️ Like and Comment 🧍 Subscribe if you are new on the channel!" + # reddit​ #redditstories​ #askreddit​ #shorts​ + titlebBeggining = "r/askReddit: " # 13 characters + + options = {} + uploaded = 0 + for i, video in enumerate(video_data): + if video["uploaded"] == False: + if uploaded < 2: + prev_video = list(video_data)[i - 1] + ############# 2022-10-19 16:39:46.887878 + format_data = "%Y-%m-%d %H:%M:%S" + # print("prev_video", prev_video) + # print("video", video) + next_upload = datetime.strptime( + prev_video["uploaded_at"], format_data + ) + timedelta(hours=4) + # print(next_upload) + titleEnding = "..." + if len(video["reddit_title"]) < 84: + titleEnding = "?" + # print(len(video["reddit_title"])) + + options = { + "file": video["filename"], + "title": titlebBeggining + + video["reddit_title"][0:84] + + titleEnding, + "description": descriptionBeginning + + video["reddit_title"] + + descriptionEnd, + "category": "22", + "keywords": "reddit,shorts,askReddit", + "privacyStatus": "private", + "publishTime": next_upload.isoformat() + # June 19th, 2022 at 8:20 AM + } + + args = argparser.parse_args() + + if not os.path.exists(options["file"]): + print(options["file"]) + exit("Please specify a valid file using the --file= parameter ") + + youtube = get_authenticated_service( + args + ) # args must be a new implementation of args object + try: + initialize_upload(youtube, options, i, next_upload) + uploaded += 1 + print(options["title"]) + video["uploaded_at"] = next_upload.strftime(format_data) + + except HttpError as e: + print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content)) diff --git a/utils/videos.py b/utils/videos.py index 7c756fc..68e9229 100755 --- a/utils/videos.py +++ b/utils/videos.py @@ -19,7 +19,9 @@ def check_done( Returns: Submission|None: Reddit object in args """ - with open("./video_creation/data/videos.json", "r", encoding="utf-8") as done_vids_raw: + with open( + "./video_creation/data/videos.json", "r", encoding="utf-8" + ) as done_vids_raw: done_videos = json.load(done_vids_raw) for video in done_videos: if video["id"] == str(redditobj): @@ -33,7 +35,9 @@ def check_done( return redditobj -def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str): +def save_data( + subreddit: str, filename: str, reddit_title: str, reddit_id: str, credit: str +): """Saves the videos that have already been generated to a JSON file in video_creation/data/videos.json Args: @@ -54,6 +58,7 @@ def save_data(subreddit: str, filename: str, reddit_title: str, reddit_id: str, "background_credit": credit, "reddit_title": reddit_title, "filename": filename, + "uploaded": False, } done_vids.append(payload) raw_vids.seek(0)