From a5fd44d4522bd056cb1b419295d6bf57cac8eccf Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Fri, 3 Dec 2021 10:32:35 +0100 Subject: [PATCH 01/11] Now on ClarityCoders' autotube fork --- .env_sample | 12 + .gitignore | 248 +++++++++++++++- MovieMaker/MovieMaker.py | 111 +++++++ MovieMaker/__init__.py | 1 + MovieMaker/utils.py | 32 ++ Publishers/YoutubePublisher.py | 149 ++++++++++ Publishers/__init__.py | 1 + README.md | 277 ++++++++++++++++-- RedditDownloader/RedditBot.py | 241 +++++++++++++++ RedditDownloader/ScaleImages.py | 60 ++++ RedditDownloader/__init__.py | 1 + .../MovieMakerExceptions.py | 33 +++ .../RedditBotExceptions.py | 28 ++ .../YoutubePublisherExceptions.py | 19 ++ RedditDownloaderExceptions/__init__.py | 3 + main.py | 73 +---- main.py-oauth2.json | 26 ++ {Music => musics}/music0.mp3 | Bin {Music => musics}/music1.mp3 | Bin {Music => musics}/music2.mp3 | Bin {Music => musics}/music3.mp3 | Bin {Music => musics}/music4.mp3 | Bin {Music => musics}/notification.mp3 | Bin requierments.txt | 4 + requirements.txt | 37 --- utils/CreateMovie.py | 111 ------- utils/RedditBot.py | 128 -------- utils/Scalegif.py | 45 --- utils/__init__.py | 62 ++++ utils/upload_video.py | 151 ---------- 30 files changed, 1300 insertions(+), 553 deletions(-) create mode 100644 .env_sample create mode 100644 MovieMaker/MovieMaker.py create mode 100644 MovieMaker/__init__.py create mode 100644 MovieMaker/utils.py create mode 100644 Publishers/YoutubePublisher.py create mode 100644 Publishers/__init__.py create mode 100644 RedditDownloader/RedditBot.py create mode 100644 RedditDownloader/ScaleImages.py create mode 100644 RedditDownloader/__init__.py create mode 100644 RedditDownloaderExceptions/MovieMakerExceptions.py create mode 100644 RedditDownloaderExceptions/RedditBotExceptions.py create mode 100644 RedditDownloaderExceptions/YoutubePublisherExceptions.py create mode 100644 RedditDownloaderExceptions/__init__.py create mode 100644 main.py-oauth2.json rename {Music => musics}/music0.mp3 (100%) rename {Music => musics}/music1.mp3 (100%) rename {Music => musics}/music2.mp3 (100%) rename {Music => musics}/music3.mp3 (100%) rename {Music => musics}/music4.mp3 (100%) rename {Music => musics}/notification.mp3 (100%) create mode 100644 requierments.txt delete mode 100644 requirements.txt delete mode 100644 utils/CreateMovie.py delete mode 100644 utils/RedditBot.py delete mode 100644 utils/Scalegif.py create mode 100644 utils/__init__.py delete mode 100644 utils/upload_video.py diff --git a/.env_sample b/.env_sample new file mode 100644 index 0000000..255b42b --- /dev/null +++ b/.env_sample @@ -0,0 +1,12 @@ +# reddit credentials +REDDIT_CLIENT_SECRET="" +REDDIT_CLIENT_ID="" +REDDIT_USER_AGENT="" + +# Magick binaries path +# Change it, it may be different +IMAGEMAGICK_BINARY="C:\Program Files\ImageMagick-7.1.0-Q16-HDRI\magick.exe" + +# Google +GOOGLE_CLIENT_ID="" +GOOGLE_CLIENT_SECRET="" \ No newline at end of file diff --git a/.gitignore b/.gitignore index b9e4ca9..0c5b0c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,242 @@ -/env + +# Created by https://www.toptal.com/developers/gitignore/api/pycharm+all,python +# Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all,python + +### PyCharm+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### PyCharm+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments .env -/data -__pycache__ -client_secrets.json -*.json -*.mp4 -video.mp4 \ No newline at end of file +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# specific to project +data/ +moviepy-master/ +videos/ +dump.txt +# End of https://www.toptal.com/developers/gitignore/api/pycharm+all,python diff --git a/MovieMaker/MovieMaker.py b/MovieMaker/MovieMaker.py new file mode 100644 index 0000000..bfaa403 --- /dev/null +++ b/MovieMaker/MovieMaker.py @@ -0,0 +1,111 @@ +import os.path +import platform +import random +from pathlib import Path +from secrets import token_hex +from typing import List + +from environs import Env +from moviepy.audio.AudioClip import CompositeAudioClip +from moviepy.editor import ImageSequenceClip, VideoFileClip, concatenate_videoclips, TextClip, AudioFileClip +from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip + +from RedditDownloaderExceptions import MissingImageMagickBinariesException, MissingMP3FilesInMusicsDir +from .utils import Utils + + +class CreateMovie(Utils): + def __init__(self, env: Env, base_path: str): + Utils.__init__(self) + self.__env = env + self.__path = base_path + self.__music_path = os.path.join(self.__path, "musics\\") + self.__video_path = os.path.join(self.__path, "videos\\") + if not os.path.isdir(self.__video_path): + os.mkdir(self.__video_path) + + if not os.path.isdir(self.__music_path): + os.mkdir(self.__music_path) + + def _create_video(self, submission_data: List[dict]) -> str: + + # check if magick binaries exists if on windows + if platform.system() == "Windows" and ( + not self.__env("IMAGEMAGICK_BINARY") or not os.path.isfile(self.__env("IMAGEMAGICK_BINARY"))): + raise MissingImageMagickBinariesException(self.__env("IMAGEMAGICK_BINARY")) + + # check if there's music inside the musics dir + if not list(Path(self.__music_path).rglob(".mp3")): + raise MissingMP3FilesInMusicsDir(self.__music_path) + + clips = [] + for submission in submission_data: + if "gif" not in submission["image_path"][-3:]: + clip = ImageSequenceClip([submission["image_path"]], durations=[12]) + clips.append(clip) + continue + clip_lengthener = [VideoFileClip(submission["image_path"])] * 60 + clip = concatenate_videoclips(clip_lengthener).subclip(0, 12) + clips.append(clip) + + clip = concatenate_videoclips(clips).subclip(0, 60) + colors = ['yellow', 'LightGreen', 'LightSkyBlue', 'LightPink4', 'SkyBlue2', 'MintCream', 'LimeGreen', + 'WhiteSmoke', 'HotPink4', 'PeachPuff3', 'OrangeRed3', 'silver'] + random.shuffle(colors) + + text_clips = [] + notification_sounds = [] + + for i, post in enumerate(submission_data): + return_comment, return_count = self._add_return_comment(post['Best_comment']) + + txt = TextClip(return_comment, font='Courier', + fontsize=38, color=colors.pop(), bg_color='black') + txt = txt.on_color(col_opacity=.3) + txt = txt.set_position((5, 500)) + txt = txt.set_start((0, 3 + (i * 12))) # (min, s) + txt = txt.set_duration(7) + txt = txt.crossfadein(0.5) + txt = txt.crossfadeout(0.5) + text_clips.append(txt) + + return_comment, _ = self._add_return_comment(post['best_reply']) + + txt = TextClip(return_comment, font='Courier', + fontsize=38, color=colors.pop(), bg_color='black') + txt = txt.on_color(col_opacity=.3) + txt = txt.set_position((15, 585 + (return_count * 50))) + txt = txt.set_start((0, 5 + (i * 12))) # (min, s) + txt = txt.set_duration(7) + txt = txt.crossfadein(0.5) + txt = txt.crossfadeout(0.5) + text_clips.append(txt) + + notification = AudioFileClip(os.path.join(self.__music_path, f"notification.mp3")) + notification = notification.set_start((0, 3 + (i * 12))) + notification_sounds.append(notification) + notification = AudioFileClip(os.path.join(self.__music_path, f"notification.mp3")) + notification = notification.set_start((0, 5 + (i * 12))) + notification_sounds.append(notification) + + music_file = os.path.join(self.__music_path, f"music{random.randint(0, 4)}.mp3") + music = AudioFileClip(music_file) + music = music.set_start((0, 0)) + music = music.volumex(.4) + music = music.set_duration(59) + + new_audioclip = CompositeAudioClip([music] + notification_sounds) + filename = token_hex(4) + filename_clips = filename + "_clips.mp4" + filename += ".mp4" + clip.write_videofile(f"{self.__video_path}{filename_clips}", fps=24) + + clip = VideoFileClip(f"{self.__video_path}{filename_clips}", audio=False) + clip = CompositeVideoClip([clip] + text_clips) + clip.audio = new_audioclip + clip.write_videofile(f"{self.__video_path}{filename}", fps=24) + + if os.path.exists(os.path.join(self.__video_path, f"{filename_clips}")): + os.remove(os.path.join(self.__video_path, f"{filename_clips}")) + + return self.__video_path + filename diff --git a/MovieMaker/__init__.py b/MovieMaker/__init__.py new file mode 100644 index 0000000..4d7270e --- /dev/null +++ b/MovieMaker/__init__.py @@ -0,0 +1 @@ +from .MovieMaker import CreateMovie diff --git a/MovieMaker/utils.py b/MovieMaker/utils.py new file mode 100644 index 0000000..4f3c5b5 --- /dev/null +++ b/MovieMaker/utils.py @@ -0,0 +1,32 @@ +from typing import Tuple + + +class Utils(object): + def __init__(self): + pass + + @staticmethod + def _get_day_suffix(day: int) -> str: + if day == 1 or day == 21 or day == 31: + return "st" + elif day == 2 or day == 22: + return "nd" + elif day == 3 or day == 23: + return "rd" + else: + return "th" + + @staticmethod + def _add_return_comment(comment: str) -> Tuple[str, int]: + need_return = 30 + new_comment = "" + return_added = 0 + if comment: + return_added += comment.count('\n') + for i, letter in enumerate(comment): + if i > need_return and letter == " ": + letter = "\n" + need_return += 30 + return_added += 1 + new_comment += letter + return new_comment, return_added diff --git a/Publishers/YoutubePublisher.py b/Publishers/YoutubePublisher.py new file mode 100644 index 0000000..8e17704 --- /dev/null +++ b/Publishers/YoutubePublisher.py @@ -0,0 +1,149 @@ +import json +import os +import random +import sys +import time +from secrets import token_hex + +import httplib2 +from environs import Env +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError +from googleapiclient.http import MediaFileUpload +from oauth2client.client import flow_from_clientsecrets +from oauth2client.file import Storage +from oauth2client.tools import run_flow + + +class YtbPublisher(object): + httplib2.RETRIES = 1 + __MAX_RETRIES = 10 + __RETRIABLE_STATUS_CODES = [500, 502, 503, 504] + __RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError) + __YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload" + __YOUTUBE_API_SERVICE_NAME = "youtube" + __YOUTUBE_API_VERSION = "v3" + __MISSING_CLIENT_SECRETS_MESSAGE = "WRONG CREDENTIALS FOR THE YOUTUBE API" + __VALID_PRIVACY_STATUSES = ("public", "private", "unlisted") + + def __init__(self, env: Env): + self.__env = env + self.__json_secret = "" + if not env("GOOGLE_CLIENT_ID") or not env("GOOGLE_CLIENT_SECRET"): + print("WARNING : Google credentials not added to .env . If you use " + ".publish_on(SocialMedias.YouTube) it will raise an error !") + + def __get_authenticated_service(self): + flow = flow_from_clientsecrets(self.__json_secret, + scope=self.__YOUTUBE_UPLOAD_SCOPE, + message=self.__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) + + return build(self.__YOUTUBE_API_SERVICE_NAME, self.__YOUTUBE_API_VERSION, + http=credentials.authorize(httplib2.Http())) + + def __initialize_upload(self, youtube, options): + 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'] + ) + ) + + # 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) + ) + + self.__resumable_upload(insert_request) + + # This method implements an exponential backoff strategy to resume a + # failed upload. + def __resumable_upload(self, insert_request): + 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']) + else: + exit("The upload failed with an unexpected response: %s" % response) + except HttpError as e: + if e.resp.status in self.__RETRIABLE_STATUS_CODES: + error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status, + e.content) + else: + raise + except self.__RETRIABLE_EXCEPTIONS as e: + error = "A retriable error occurred: %s" % e + + if error is not None: + print(error) + retry += 1 + if retry > self.__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 _youtube(self, video_data): + if not os.path.exists(video_data['file']): + exit("Please specify a valid file using the file= parameter in the data passed to .publish_on().") + + client_json = { + "web": { + "client_id": self.__env("GOOGLE_CLIENT_ID"), + "client_secret": self.__env("GOOGLE_CLIENT_SECRET"), + "redirect_uris": [], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token" + } + } + self.__json_secret = f"{token_hex(4)}.json" + with open(self.__json_secret, encoding="utf-8", mode="w") as f: + json.dump(client_json, f) + + youtube = self.__get_authenticated_service() + try: + self.__initialize_upload(youtube, video_data) + + except HttpError as e: + print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content)) + if os.path.isfile(self.__json_secret): + os.remove(self.__json_secret) + + +if __name__ == "__main__": + env = Env() + env.read_env() + ytb = YtbPublisher(env) + data = { + "file": "C:\\Users\\julien.gunther\\PycharmProjects\\RedditDownloader\\videos\\67f2c04d.mp4", + "title": "#shorts \n Memes but this time you laugh for real", + "description": "why tho", + "keywords": "meme,memes,laugh,internet,short", + "privacyStatus": "private" + } + ytb._youtube(data) diff --git a/Publishers/__init__.py b/Publishers/__init__.py new file mode 100644 index 0000000..18b723c --- /dev/null +++ b/Publishers/__init__.py @@ -0,0 +1 @@ +from .YoutubePublisher import YtbPublisher diff --git a/README.md b/README.md index 7bf74e3..50fe352 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,260 @@ - -# Fully Automated YouTube Shorts Channel -> This code will show you how to setup and fully autmated YouTube Channel. -> Content is gathered from Reddit using images both animated and still images will work. -> Setup is in depth at times so check out the tutorial video if you need extra help. - -## Setup -- Clone Project -- pip install -r requirements.txt -- This project uses MoviePy which uses ImageMagick to write text onto our movie. Follow the link to install. Not on windows you will need to update config defaults. -- create a .env file with your reddit API details. -- create a client_secrets.json for your YouTube API details. -- Both are covered in the video if you need help! - -## Contact! -- YouTube Clarity Coders -- Chat with me! Discord +# Reddit Image Downloader and publisher + +## A follow up to [ClarityCoders' AutoTube](https://github.com/ClarityCoders/AutoTube) + +### Requirements + +You need a reddit application for that. You can create one to the following link :https://www.reddit.com/prefs/apps + +You can follow this tutorial to create your application : https://youtu.be/bMT9ZC9sBzI?t=228 + +### Installation steps + +1. Clone this repository +2. Install [ImageMagick](https://www.imagemagick.org/script/index.php) +3. Rename the [.env_sample](.env_sample) file to .env +4. Edit your .env file : + - REDDIT_CLIENT_SECRET="YourClientSecret" + - REDDIT_CLIENT_ID="YourClientId" + - REDDIT_USER_AGENT="" + - IMAGEMAGICK_BINARIES="C:\Program Files\ImageMagick-7.1.0-Q16-HDRI\magick.exe" (Change the path to your + installation to the convert.exe file. Check [moviepy on pypi](https://pypi.org/project/moviepy/) for more + information) + +The `IMAGEMAGICK_BINARIES` environment variable is only needed if you're on Windows or on Ubuntu 16.04LTS + +An example for the reddit user agent : "" + +4. open a console (bash, cmd, etc...) where you cloned the repo and enter the following command : + +`pip install -r requirements.txt` + +If it returns an error, try the following command : + +`pip3 install -r requirements.txt` + +Is it still doesn't work, make sure you that python and pip are properly installed. + +## How to use + +Some code will be more explicit : + +````python +from RedditDownloader import RedditBot +from utils import SocialMedias, get_credentials, Scales + +reddit = RedditBot(get_credentials()) +data = reddit.save_images_from_subreddit( + amount=5, + subreddits=("dankmemes",), + scale=Scales.InstagramPhotoSquare +) +video_path = reddit.create_video(data) + +ytb_data = { + "file": video_path, + "title": "#short \n Memes but this time you laugh for real", + "description": "why tho", + "keywords": "meme,memes,laugh,internet,short", + "privacyStatus": "public" +} + +reddit.publish_on(SocialMedias.YouTube, ytb_data) + +```` + +## RedditBot + +This class takes 2 arguments : + +- required : `env` - `environs.Env` instance that has been initialized. There's a utility function for that : + `utils.get_credentials()` +- optional : `log` - `True` to log to console operation, `False` by default + +You then have a single method described below : + +## RedditBot.save_images_from_subreddit() + +All 6 keyword arguments are optional. + +- `subreddits` - a tuple of strings that contains the subreddit names. By default, it will query the + [memes](https://www.reddit.com/r/memes/) subreddit. Check out what's after the r/ to known what is the exact string to + add to the tuple. + + +- `amount` - how many images (posts) do you want to download (int). By default, 5. + + +- `filetypes` - A tuple that contains all the file extensions that you want to download. By default, + ("jpg", "png", "gif"). It may be used to download only jps and png or only gif. It may raise errors with other file + extensions. + + +- `nsfw` - bool. If set to True it will only download NSFW posts (marked NSFW by the community or mods). If set to False + it will only download SFW posts. By default, set to False. + + +- `scale` - tuple of ints. If passed, it will resize the downloaded images with the size passed as argument + (width, height). By default, None is passed so no resize occurs. Some sizes are already defined for TikTok, YouTube or + Instagram. -> see [Scales](#scales) + + +- `replace_resized` - bool. Only works when a new scale is passed. If set to False the resized image will be placed in + a "resized" folder along with the original images. If set to True it will replace the original images. + +This method will return a `list of dict` with the data queried that you can use later on. + +## RedditBot.create_video() + +This method will create a video with the data previously queried. it takes an optional argument : + +- video_data : that correspond to the data queried. If no argument are passed, it will take the last data queried if + still in memory. If no argument are passed and there is no data left in memory, it will raise an exception. + +It returns the path to the created video. + +## RedditBot.get_path_images() + +This method will return the path of the queried images. it takes one optional argument : + +- data : that correspond to the data queried. If no argument are passed, it will take the last data queried if still in + memory. If no argument are passed and there is no data left in memory, it will raise an exception. + +## RedditBot.publish_on() + +Publish photo or video on a social media. + +Takes 2 required arguments : + +- social_media - see [SocialMedias](#SocialMedias) +- data - a dict that contains all the data needed for the post. See [Data format](#data-format) for more information + about the data formatting. + +## Scales + +There's some scales already defined. To access them, import `Scales` from `utils` : + +````python +from utils import Scales + +Scales.show_attributes() +```` + +````text +['Default', + 'InstagramIGTVCoverPhoto', + 'InstagramPhotoLandscape', + 'InstagramPhotoPortrait', + 'InstagramPhotoSquare', + 'InstagramReels', + 'InstagramStories', + 'InstagramVideoLandscape', + 'InstagramVideoPortrait', + 'InstagramVideoSquare', + 'Snapchat', + 'TikTok', + 'YoutubeShortsFullscreen', + 'YoutubeShortsSquare', + 'YoutubeVideo'] +```` + +## SocialMedias + +Use this class to choose a social media to posts your video/photo + +````python +from utils import SocialMedias + +SocialMedias.show_attributes() +```` + +````text +['Instagram', 'Snapchat', 'TikTok', 'YouTube'] +```` + +usage : + +````python +from RedditDownloader import RedditBot +from utils import SocialMedias, get_credentials, Scales + +reddit = RedditBot(get_credentials()) +data = reddit.save_images_from_subreddit( + amount=5, + subreddits=("dankmemes",), + scale=Scales.InstagramPhotoSquare +) +video_path = reddit.create_video(data) + +ytb_data = { + "file": video_path, + "title": "#short \n Memes but this time you laugh for real", + "description": "why tho", + "keywords": "meme,memes,laugh,internet,short", + "privacyStatus": "public" +} + +reddit.publish_on(SocialMedias.YouTube, ytb_data) + +```` + +## Data format + +### for create_video() + +````json +[ + { + "image_path": "absolute path to image", + "Best_comment": "best comment on this image", + "best_reply": "best reply of the best comment" + }, + { + "image_path": "absolute path to image", + "Best_comment": "best comment on this image", + "best_reply": "best reply of the best comment" + }, + ... +] +```` + +### for publish_on(SocialMedias.YouTube) +#### still not implemented +````json + +{ + "file": "absolute path to video", + "title": "video title", + "description": "video description", + "keywords": "tag1,tag2,tag3...", + "privacyStatus": "private|public" +} +```` + +### for publish_on(SocialMedias.Instagram) +#### still not implemented +````json +{ + "files": ["absolute path to video/photo1", "absolute path to video/photo2", ...], + "description": "post description", + "type": "igtv|reels|post" +} +```` + +### for publish_on(SocialMedias.TikTok) +#### still not implemented +````json +{ + "file": "absolute path to video", + "description": "post description" +} +```` + +### for publish_on(SocialMedias.Snapchat) +#### still not implemented +````json +{ + "file": "absolute path to video", + "description": "post description" +} +```` \ No newline at end of file diff --git a/RedditDownloader/RedditBot.py b/RedditDownloader/RedditBot.py new file mode 100644 index 0000000..fcd782a --- /dev/null +++ b/RedditDownloader/RedditBot.py @@ -0,0 +1,241 @@ +import json +import os +from datetime import date +from typing import List, Tuple + +import praw +import requests +from environs import Env + +from MovieMaker import CreateMovie +from Publishers import YtbPublisher +from RedditDownloaderExceptions import MissingCredentialsException, IncorrectCredentialsException +from .ScaleImages import Scale +from prawcore import ResponseException + + +class RedditBot(Scale, CreateMovie, YtbPublisher): + def __init__(self, env: Env, base_path=os.getcwd(), log: bool = False) -> None: + """Reddit downloader class + + :param env: environs.Env object that already has been initialized. You can use utils.get_credentials() for that. + :param log: True to log operations in console, by default to False. + :param base_path: A string or pathlike to a folder where images and data will be downloaded (cwd by default). + """ + + # init parent classes + Scale.__init__(self) + CreateMovie.__init__(self, env, base_path) + YtbPublisher.__init__(self, env) + + self._log = log + + # Check if credentials exists + if not env("REDDIT_CLIENT_ID"): + raise MissingCredentialsException("REDDIT_CLIENT_ID") + if not env("REDDIT_CLIENT_SECRET"): + raise MissingCredentialsException("REDDIT_CLIENT_SECRET") + if not env("REDDIT_USER_AGENT"): + raise MissingCredentialsException("REDDIT_USER_AGENT") + + # connect to reddit + self.__reddit = praw.Reddit( + client_id=env("REDDIT_CLIENT_ID"), + client_secret=env("REDDIT_CLIENT_SECRET"), + user_agent=env("REDDIT_USER_AGENT") + ) + + try: + self.__reddit.user.me() + except ResponseException: + raise IncorrectCredentialsException() + + + # define image format that we want to query + self.__accepted_format = ["jpg", "png", "gif"] + + # define a path with folder that has today's date + self.__today_data_path = os.path.join(base_path, f"data\\{date.today().strftime('%m%d%Y')}\\") + + # define path to utility file like already_downloaded.json + self.__already_downloaded_path = os.path.join(base_path, f"data/utils/") + + # define file name for already downloaded images + self.__already_downloaded_json = "already_downloaded.json" + + # store downloaded data in a single list + self.__submission_data = [] + + # create already_downloaded.json if not exists + if not os.path.isdir(self.__already_downloaded_path): + os.makedirs(self.__already_downloaded_path, exist_ok=True) + with open(file=f"{self.__already_downloaded_path}{self.__already_downloaded_json}", mode="w"): + pass + + # load json file to class + with open(file=f"{self.__already_downloaded_path}{self.__already_downloaded_json}", mode="r", + encoding="utf-8-sig") as f: + try: + self.__already_downloaded = json.loads(f.read()) + except json.decoder.JSONDecodeError as e: + self.__already_downloaded = [] + + def __create_subreddit_folder(self, subreddit: str) -> str: + + sub_path = os.path.join(self.__today_data_path, f"{subreddit}") + if not os.path.isdir(sub_path): + os.makedirs(sub_path, exist_ok=True) + os.mkdir(os.path.join(sub_path, "images")) + os.mkdir(os.path.join(sub_path, "data")) + return sub_path + + def __get_posts_from_subreddit(self, subreddit: str, over_18: bool, amount: int, accepted_format: Tuple[str]) -> \ + List[praw.reddit.models.Submission]: + + submissions = [] + for submission in self.__reddit.subreddit(subreddit).top("day", limit=1000): + if not submission.stickied and submission.url.lower()[-3:] in accepted_format and \ + submission.over_18 == over_18 and submission.id not in self.__already_downloaded: + submissions.append(submission) + if len(submissions) >= amount: + break + return submissions + + def __save_submission_image(self, save_path: str, submission: praw.reddit.models.Submission, scale: tuple, + replace_resized: bool) -> None: + + img = requests.get(submission.url.lower()) + with open(save_path, "wb") as f: + f.write(img.content) + if self._log: + print("Image downloaded.") + if scale: + if self._log: + print("Resizing image...") + self._scale_image(save_path, scale, replace_resized) + + def __save_submission_data(self, save_path: str, image_path: str, + submission: praw.reddit.models.Submission) -> None: + submission.comment_sort = "best" + best_comment = None + best_comment_2 = None + best_reply = None + + for comment in submission.comments: + if len(comment.body) <= 140 and "http" not in comment.body: + if not best_comment: + best_comment = comment + else: + best_comment_2 = comment.body + break + + if best_comment: + best_comment.reply_sort = "top" + best_comment.refresh() + + for reply in best_comment.replies: + if len(reply.body) >= 140 or "http" in reply.body: + continue + best_reply = reply.body + break + + best_comment = best_comment.body + + submission_data = { + "image_path": image_path, + 'id': submission.id, + "title": submission.title, + "score": submission.score, + "18": submission.over_18, + "Best_comment": best_comment, + "Best_comment_2": best_comment_2, + "best_reply": best_reply + } + + self.__already_downloaded.append(submission.id) + self.__submission_data.append(submission_data) + with open(f"{self.__already_downloaded_path}{self.__already_downloaded_json}", mode="w", + encoding="utf-8-sig") as f: + json.dump(self.__already_downloaded, f) + with open(f"{save_path}", mode="w", encoding="utf-8-sig") as f: + json.dump(submission_data, f) + + def save_images_from_subreddit(self, subreddits: Tuple[str] = ("memes",), amount: int = 5, + filetypes: tuple = ("jpg", "png"), nsfw: bool = False, + scale: tuple = None, replace_resized: bool = True) -> List[dict]: + """Save images from multiple subreddits. + + :param subreddits: Tuple of strings that contain subreddit names (by default, will search for the /r/memes + subreddit) + :param amount: amount of posts to query by subreddit. 5 by default. + :param filetypes: a tuple of accepted file types. By default, : ("jpg", "png"). Warning ! Using other file + types than those 2 may cause exceptions. That functionality hasn't been tested. Its use is mainly to + restrain queries to one or two of the default types. + :param nsfw: True for NSFW posts only, False for SFW posts only. False by default + :param scale: a tuple (width: int, height: int) you can pass with the new width/height (in pixel) for each image + downloaded. None by default + :param replace_resized: used if a scale is passed. If True it replaces the images, if False it will create a + new directory with resized images. True by default + :return: data queried + """ + self.__submission_data = [] + for subreddit in subreddits: + save_path = self.__create_subreddit_folder(subreddit) + if self._log: + print(f"Search for images on the {subreddit} subreddit...") + submissions = self.__get_posts_from_subreddit(subreddit, nsfw, amount, filetypes) + if self._log: + print("Images found ! start downloading them...") + for submission in submissions: + image_path = f"{save_path}\\images\\{submission.id}{submission.url.lower()[-4:]}" + self.__save_submission_image(image_path, submission, scale, replace_resized) + self.__save_submission_data(f"{save_path}\\data\\{submission.id}.json", image_path, submission) + if self._log: + print(f"{len(submissions)} images from /r/{subreddit} have been downloaded.") + if self._log: + print(f"Download finished for the following subreddit(s) : {', '.join(subreddits)}.") + + return self.__submission_data + + def create_video(self, video_data: List[dict] = None): + """Create a video with the images previously saved from reddit. + + :param video_data: Optional Video data returned by .save_images_from_subreddit. If nothing passed it will + use the last posts queried from reddit. + :return: the path to the created video + """ + return self._create_video(video_data if video_data else self.__submission_data) + + def get_path_images(self, data: List[dict] = None): + """Return a list oh paths for the data passed + + :param data: optional data queried with .save_images_from_subreddit() + :return: a list of path + """ + if not data: + data = self.__submission_data + return [i["image_path"] for i in data] + + def publish_on(self, social_media: int, media_data: dict) -> None: + """Publish a video or an image on a social media + + :param social_media: use utils.SocialMedias.TheSocialMediaYouWant + :param media_data: dict that contains data about an image. It can be found in "{base_path}/data/{today}/{subreddit}/data/" + :return: None + """ + if social_media == 0: + self._youtube(media_data) + return + + if social_media == 1: + # TODO: implement TikTok + print("Not implemented yet") + return + + if social_media == 2: + # TODO: implement Instagram + print("Not implemented yet") + return + + print("Not implemented yet") + # TODO: implement Snapchat diff --git a/RedditDownloader/ScaleImages.py b/RedditDownloader/ScaleImages.py new file mode 100644 index 0000000..f7e9120 --- /dev/null +++ b/RedditDownloader/ScaleImages.py @@ -0,0 +1,60 @@ +import os + +from PIL import Image + + +class Scale(object): + def __init__(self): + pass + + def _scale_image(self, path: str, scale: tuple, replace_resized: bool): + img = Image.open(path) + filename = path.split("\\")[-1] + + if not replace_resized: + path = path.replace(filename, "resized\\") + if not os.path.isdir(path): + os.mkdir(path) + path += filename + + if path[-3:] != "gif": + img = img.resize(scale) + img.save(path) + return + + old_infos = { + "version": img.info.get("version", b"GIF01a"), + "loop": bool(img.info.get("loop", 1)), + "duration": img.info.get("duration", 40), + "background": img.info.get("background", 223), + 'extension': img.info.get('extension', b'NETSCAPE2.0'), + 'transparency': img.info.get('transparency', 223) + } + + new_frames = self.__get_new_frames(img, scale) + self.__save_new_gif(new_frames, old_infos, path) + + @staticmethod + def __get_new_frames(gif: Image, scale: tuple) -> list: + new_frames = [] + actual_frames = gif.n_frames + for frame in range(actual_frames): + gif.seek(frame) + new_frame = Image.new('RGBA', gif.size) + new_frame.paste(gif) + new_frame = new_frame.resize(scale, Image.ANTIALIAS) + new_frames.append(new_frame) + return new_frames + + @staticmethod + def __save_new_gif(frames: list, old_infos: dict, path: str): + frames[0].save( + path, + version=old_infos["version"], + append_images=frames[1:], + duration=old_infos['duration'], + loop=old_infos['loop'], + background=old_infos['background'], + extension=old_infos['extension'], + transparency=old_infos['transparency'] + ) diff --git a/RedditDownloader/__init__.py b/RedditDownloader/__init__.py new file mode 100644 index 0000000..e2fc7f9 --- /dev/null +++ b/RedditDownloader/__init__.py @@ -0,0 +1 @@ +from .RedditBot import RedditBot diff --git a/RedditDownloaderExceptions/MovieMakerExceptions.py b/RedditDownloaderExceptions/MovieMakerExceptions.py new file mode 100644 index 0000000..030a31d --- /dev/null +++ b/RedditDownloaderExceptions/MovieMakerExceptions.py @@ -0,0 +1,33 @@ +class MovieMakerException(Exception): + """Base exception for the RedditBot class + """ + + def __init__(self, *args): + Exception.__init__(self, *args) + + +class MissingImageMagickBinariesException(MovieMakerException): + """Raised when missing magick binaries + """ + + def __init__(self, path: str, + message: str = "Missing ImageMagick binaries or path not valid. Please complete your .env file."): + self.__path = path + self.__message = message + MovieMakerException.__init__(self, message) + + def __str__(self): + return f"{self.__message} : \"{self.__path}\"" + + +class MissingMP3FilesInMusicsDir(MovieMakerException): + """Raised if the musics dir doesn't contain any mp3 file + """ + + def __init__(self, path, message: str = "Missing MP3 files in"): + self.__message = message + self.__path = path + MovieMakerException.__init__(self, message) + + def __str__(self): + return f"{self.__message} {self.__path}" diff --git a/RedditDownloaderExceptions/RedditBotExceptions.py b/RedditDownloaderExceptions/RedditBotExceptions.py new file mode 100644 index 0000000..7472e14 --- /dev/null +++ b/RedditDownloaderExceptions/RedditBotExceptions.py @@ -0,0 +1,28 @@ +class RedditBotException(Exception): + """Base exception for the RedditBot class + """ + + def __init__(self, *args): + Exception.__init__(self, *args) + + +class MissingCredentialsException(RedditBotException): + """Raised when credentials for Reddit are missing + """ + + def __init__(self, credential, message="Missing reddit credentials. Please complete your .env file."): + self.__credential = credential + self.__message = message + RedditBotException.__init__(self, message) + + def __str__(self): + return f"{self.__message} : \"{self.__credential}\"" + + +class IncorrectCredentialsException(RedditBotException): + """Raised when reddit credentials are not correct + """ + + def __init__(self, + message: str = "Can't connect to reddit with current credentials. Please enter valid credentials in your .env file."): + RedditBotException.__init__(self, message) diff --git a/RedditDownloaderExceptions/YoutubePublisherExceptions.py b/RedditDownloaderExceptions/YoutubePublisherExceptions.py new file mode 100644 index 0000000..bb5c247 --- /dev/null +++ b/RedditDownloaderExceptions/YoutubePublisherExceptions.py @@ -0,0 +1,19 @@ +class YoutubePublisherException(Exception): + """Base exception for the RedditBot class + """ + + def __init__(self, *args): + Exception.__init__(self, *args) + + +class MissingCredentialsException(YoutubePublisherException): + """Raised when credentials for YouTube are missing + """ + + def __init__(self, credential, message="Missing youtube credentials. Please complete your .env file."): + self.__credential = credential + self.__message = message + YoutubePublisherException.__init__(self, message) + + def __str__(self): + return f"{self.__message} : \"{self.__credential}\"" diff --git a/RedditDownloaderExceptions/__init__.py b/RedditDownloaderExceptions/__init__.py new file mode 100644 index 0000000..93e19c9 --- /dev/null +++ b/RedditDownloaderExceptions/__init__.py @@ -0,0 +1,3 @@ +from .RedditBotExceptions import MissingCredentialsException, IncorrectCredentialsException +from .YoutubePublisherExceptions import MissingCredentialsException +from .MovieMakerExceptions import MissingImageMagickBinariesException, MissingMP3FilesInMusicsDir diff --git a/main.py b/main.py index be55260..36dfe69 100644 --- a/main.py +++ b/main.py @@ -1,57 +1,16 @@ -""" -This is the main loop file for our AutoTube Bot! - -Quick notes! -- Currently it's set to try and post a video then sleep for a day. -- You can change the size of the video currently it's set to post shorts. - * Do this by adding a parameter of scale to the image_save function. - * scale=(width,height) -""" - -from datetime import date -import time -from utils.CreateMovie import CreateMovie, GetDaySuffix -from utils.RedditBot import RedditBot -from utils.upload_video import upload_video - -#Create Reddit Data Bot -redditbot = RedditBot() - -# Leave if you want to run it 24/7 -while True: - - # Gets our new posts pass if image related subs. Default is memes - posts = redditbot.get_posts("memes") - - # Create folder if it doesn't exist - redditbot.create_data_folder() - - # Go through posts and find 5 that will work for us. - for post in posts: - redditbot.save_image(post) - - # Wanted a date in my titles so added this helper - DAY = date.today().strftime("%d") - DAY = str(int(DAY)) + GetDaySuffix(int(DAY)) - dt_string = date.today().strftime("%A %B") + f" {DAY}" - - # Create the movie itself! - CreateMovie.CreateMP4(redditbot.post_data) - - # Video info for YouTube. - # This example uses the first post title. - video_data = { - "file": "video.mp4", - "title": f"{redditbot.post_data[0]['title']} - Dankest memes and comments {dt_string}!", - "description": "#shorts\nGiving you the hottest memes of the day with funny comments!", - "keywords":"meme,reddit,Dankestmemes", - "privacyStatus":"public" - } - - print(video_data["title"]) - print("Posting Video in 5 minutes...") - time.sleep(60 * 5) - upload_video(video_data) - - # Sleep until ready to post another video! - time.sleep(60 * 60 * 24 - 1) +from RedditDownloader import RedditBot +from utils import SocialMedias, get_credentials + +reddit = RedditBot(get_credentials(), log=True) +data = reddit.save_images_from_subreddit(amount=1) +video_path = reddit.create_video(data) + +ytb_data = { + "file": video_path, + "title": "#shorts r/cursedcomments", + "description": "why tho", + "keywords": "meme,memes,laugh,internet,short,reddit", + "privacyStatus": "unlisted" +} + +reddit.publish_on(SocialMedias.YouTube, ytb_data) diff --git a/main.py-oauth2.json b/main.py-oauth2.json new file mode 100644 index 0000000..05ccb67 --- /dev/null +++ b/main.py-oauth2.json @@ -0,0 +1,26 @@ +{ + "access_token": "ya29.a0ARrdaM8CIPs-qRP0PvyOUA-_GvFVYRzj3NpppdKndviTwDkVRanLUY_6JQDNnJlaJkdWrESCbE2N_6hOxiXBIze11IKxIb6cqnDWDFgJRM-cw6Al8qHUb68m0tQjEb0c9b8jZhj3SqdMmLRsLZzlUNmC-MUy", + "client_id": "826731140207-vhi85d2ejau1vsbieh2rc3ooki2aggq6.apps.googleusercontent.com", + "client_secret": "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", + "refresh_token": "1//09G5Ur8xWhnSWCgYIARAAGAkSNwF-L9IrvvqdALHOYgzYes_FNUMuU_iXDG2abH-uG76mD-WKG9m1WXFkYuKAeW3WCOsRwWB_sWE", + "token_expiry": "2021-12-02T21:47:50Z", + "token_uri": "https://accounts.google.com/o/oauth2/token", + "user_agent": null, + "revoke_uri": "https://oauth2.googleapis.com/revoke", + "id_token": null, + "id_token_jwt": null, + "token_response": { + "access_token": "ya29.a0ARrdaM8CIPs-qRP0PvyOUA-_GvFVYRzj3NpppdKndviTwDkVRanLUY_6JQDNnJlaJkdWrESCbE2N_6hOxiXBIze11IKxIb6cqnDWDFgJRM-cw6Al8qHUb68m0tQjEb0c9b8jZhj3SqdMmLRsLZzlUNmC-MUy", + "expires_in": 3599, + "refresh_token": "1//09G5Ur8xWhnSWCgYIARAAGAkSNwF-L9IrvvqdALHOYgzYes_FNUMuU_iXDG2abH-uG76mD-WKG9m1WXFkYuKAeW3WCOsRwWB_sWE", + "scope": "https://www.googleapis.com/auth/youtube.upload", + "token_type": "Bearer" + }, + "scopes": [ + "https://www.googleapis.com/auth/youtube.upload" + ], + "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", + "invalid": false, + "_class": "OAuth2Credentials", + "_module": "oauth2client.client" +} \ No newline at end of file diff --git a/Music/music0.mp3 b/musics/music0.mp3 similarity index 100% rename from Music/music0.mp3 rename to musics/music0.mp3 diff --git a/Music/music1.mp3 b/musics/music1.mp3 similarity index 100% rename from Music/music1.mp3 rename to musics/music1.mp3 diff --git a/Music/music2.mp3 b/musics/music2.mp3 similarity index 100% rename from Music/music2.mp3 rename to musics/music2.mp3 diff --git a/Music/music3.mp3 b/musics/music3.mp3 similarity index 100% rename from Music/music3.mp3 rename to musics/music3.mp3 diff --git a/Music/music4.mp3 b/musics/music4.mp3 similarity index 100% rename from Music/music4.mp3 rename to musics/music4.mp3 diff --git a/Music/notification.mp3 b/musics/notification.mp3 similarity index 100% rename from Music/notification.mp3 rename to musics/notification.mp3 diff --git a/requierments.txt b/requierments.txt new file mode 100644 index 0000000..9be551e --- /dev/null +++ b/requierments.txt @@ -0,0 +1,4 @@ +praw~=7.5.0 +environs~=9.3.5 +pillow~=8.4.0 +moviepy~=1.0.3 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index cef3ce2..0000000 --- a/requirements.txt +++ /dev/null @@ -1,37 +0,0 @@ -cachetools==4.2.4 -certifi==2021.10.8 -charset-normalizer==2.0.7 -colorama==0.4.4 -decorator==4.4.2 -google-api-core==2.2.2 -google-api-python-client==2.31.0 -google-auth==2.3.3 -google-auth-httplib2==0.1.0 -google-auth-oauthlib==0.4.6 -googleapis-common-protos==1.53.0 -httplib2==0.20.2 -idna==3.3 -imageio==2.11.1 -imageio-ffmpeg==0.4.5 -moviepy==1.0.3 -numpy==1.21.4 -oauth2client==4.1.3 -oauthlib==3.1.1 -Pillow==8.4.0 -praw==7.5.0 -prawcore==2.3.0 -proglog==0.1.9 -protobuf==3.19.1 -pyasn1==0.4.8 -pyasn1-modules==0.2.8 -pyparsing==3.0.6 -python-dotenv==0.19.2 -requests==2.26.0 -requests-oauthlib==1.3.0 -rsa==4.7.2 -six==1.16.0 -tqdm==4.62.3 -update-checker==0.18.0 -uritemplate==4.1.1 -urllib3==1.26.7 -websocket-client==1.2.1 diff --git a/utils/CreateMovie.py b/utils/CreateMovie.py deleted file mode 100644 index 0f878c4..0000000 --- a/utils/CreateMovie.py +++ /dev/null @@ -1,111 +0,0 @@ -from moviepy.editor import * -import random -import os - -dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - -def GetDaySuffix(day): - if day == 1 or day == 21 or day == 31: - return "st" - elif day == 2 or day == 22: - return "nd" - elif day == 3 or day == 23: - return "rd" - else: - return "th" - -dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) -music_path = os.path.join(dir_path, "Music/") - -def add_return_comment(comment): - need_return = 30 - new_comment = "" - return_added = 0 - return_added += comment.count('\n') - for i, letter in enumerate(comment): - if i > need_return and letter == " ": - letter = "\n" - need_return += 30 - return_added += 1 - new_comment += letter - return new_comment, return_added - - -class CreateMovie(): - - @classmethod - def CreateMP4(cls, post_data): - - clips = [] - for post in post_data: - if "gif" not in post['image_path']: - clip = ImageSequenceClip([post['image_path']], durations=[12]) - clips.append(clip) - else: - clip = VideoFileClip(post['image_path']) - clip_lengthener = [clip] * 60 - clip = concatenate_videoclips(clip_lengthener) - clip = clip.subclip(0,12) - clips.append(clip) - - # After we have out clip. - clip = concatenate_videoclips(clips) - - # Hack to fix getting extra frame errors?? - clip = clip.subclip(0,60) - - colors = ['yellow', 'LightGreen', 'LightSkyBlue', 'LightPink4', 'SkyBlue2', 'MintCream','LimeGreen', 'WhiteSmoke', 'HotPink4'] - colors = colors + ['PeachPuff3', 'OrangeRed3', 'silver'] - random.shuffle(colors) - text_clips = [] - notification_sounds = [] - for i, post in enumerate(post_data): - return_comment, return_count = add_return_comment(post['Best_comment']) - txt = TextClip(return_comment, font='Courier', - fontsize=38, color=colors.pop(), bg_color='black') - txt = txt.on_color(col_opacity=.3) - txt = txt.set_position((5,500)) - txt = txt.set_start((0, 3 + (i * 12))) # (min, s) - txt = txt.set_duration(7) - txt = txt.crossfadein(0.5) - txt = txt.crossfadeout(0.5) - text_clips.append(txt) - return_comment, _ = add_return_comment(post['best_reply']) - txt = TextClip(return_comment, font='Courier', - fontsize=38, color=colors.pop(), bg_color='black') - txt = txt.on_color(col_opacity=.3) - txt = txt.set_position((15,585 + (return_count * 50))) - txt = txt.set_start((0, 5 + (i * 12))) # (min, s) - txt = txt.set_duration(7) - txt = txt.crossfadein(0.5) - txt = txt.crossfadeout(0.5) - text_clips.append(txt) - notification = AudioFileClip(os.path.join(music_path, f"notification.mp3")) - notification = notification.set_start((0, 3 + (i * 12))) - notification_sounds.append(notification) - notification = AudioFileClip(os.path.join(music_path, f"notification.mp3")) - notification = notification.set_start((0, 5 + (i * 12))) - notification_sounds.append(notification) - - music_file = os.path.join(music_path, f"music{random.randint(0,4)}.mp3") - music = AudioFileClip(music_file) - music = music.set_start((0,0)) - music = music.volumex(.4) - music = music.set_duration(59) - - new_audioclip = CompositeAudioClip([music]+notification_sounds) - clip.write_videofile(f"video_clips.mp4", fps = 24) - - clip = VideoFileClip("video_clips.mp4",audio=False) - clip = CompositeVideoClip([clip] + text_clips) - clip.audio = new_audioclip - clip.write_videofile("video.mp4", fps = 24) - - - if os.path.exists(os.path.join(dir_path, "video_clips.mp4")): - os.remove(os.path.join(dir_path, "video_clips.mp4")) - else: - print(os.path.join(dir_path, "video_clips.mp4")) - -if __name__ == '__main__': - print(TextClip.list('color')) \ No newline at end of file diff --git a/utils/RedditBot.py b/utils/RedditBot.py deleted file mode 100644 index 3821fd9..0000000 --- a/utils/RedditBot.py +++ /dev/null @@ -1,128 +0,0 @@ -from datetime import date -import os -import praw -from dotenv import load_dotenv -import requests -import json -from utils.Scalegif import scale_gif - -load_dotenv() - - -class RedditBot(): - - def __init__(self): - self.reddit = praw.Reddit( - client_id=os.getenv('client_id'), - client_secret=os.getenv('client_secret'), - user_agent=os.getenv('user_agent'), - ) - - dir_path = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) - self.data_path = os.path.join(dir_path, "data/") - self.post_data = [] - self.already_posted = [] - - # Check for a posted_already.json file - self.posted_already_path = os.path.join( - self.data_path, "posted_already.json") - if os.path.isfile(self.posted_already_path): - print("Loading posted_already.json from data folder.") - with open(self.posted_already_path, "r") as file: - self.already_posted = json.load(file) - - def get_posts(self, sub="memes"): - self.post_data = [] - subreddit = self.reddit.subreddit(sub) - posts = [] - for submission in subreddit.top("day", limit=100): - if submission.stickied: - print("Mod Post") - else: - posts.append(submission) - - return posts - - def create_data_folder(self): - today = date.today() - dt_string = today.strftime("%m%d%Y") - data_folder_path = os.path.join(self.data_path, f"{dt_string}/") - check_folder = os.path.isdir(data_folder_path) - # If folder doesn't exist, then create it. - if not check_folder: - os.makedirs(data_folder_path) - - def save_image(self, submission, scale=(720, 1280)): - if "jpg" in submission.url.lower() or "png" in submission.url.lower() or "gif" in submission.url.lower() and "gifv" not in submission.url.lower(): - # try: - - # Get all images to ignore - dt_string = date.today().strftime("%m%d%Y") - data_folder_path = os.path.join(self.data_path, f"{dt_string}/") - CHECK_FOLDER = os.path.isdir(data_folder_path) - if CHECK_FOLDER and len(self.post_data) < 5 and not submission.over_18 and submission.id not in self.already_posted: - image_path = f"{data_folder_path}Post-{submission.id}{submission.url.lower()[-4:]}" - - # Get the image and write the path - reqest = requests.get(submission.url.lower()) - with open(image_path, 'wb') as f: - f.write(reqest.content) - - # Could do transforms on images like resize! - #image = cv2.resize(image,(720,1280)) - scale_gif(image_path, scale) - - #cv2.imwrite(f"{image_path}", image) - submission.comment_sort = 'best' - - # Get best comment. - best_comment = None - best_comment_2 = None - - for top_level_comment in submission.comments: - # Here you can fetch data off the comment. - # For the sake of example, we're just printing the comment body. - if len(top_level_comment.body) <= 140 and "http" not in top_level_comment.body: - if best_comment is None: - best_comment = top_level_comment - else: - best_comment_2 = top_level_comment - break - - best_comment.reply_sort = "top" - best_comment.refresh() - replies = best_comment.replies - - best_reply = None - for top_level_comment in replies: - # Here you can fetch data off the comment. - # For the sake of example, we're just printing the comment body. - best_reply = top_level_comment - if len(best_reply.body) <= 140 and "http" not in best_reply.body: - break - - if best_reply is not None: - best_reply = best_reply.body - else: - best_reply = "MIA" - if best_comment_2 is not None: - best_reply = best_comment_2.body - - data_file = { - "image_path": image_path, - 'id': submission.id, - "title": submission.title, - "score": submission.score, - "18": submission.over_18, - "Best_comment": best_comment.body, - "best_reply": best_reply - } - - self.post_data.append(data_file) - self.already_posted.append(submission.id) - with open(f"{data_folder_path}{submission.id}.json", "w") as outfile: - json.dump(data_file, outfile) - with open(self.posted_already_path, "w") as outfile: - json.dump(self.already_posted, outfile) - else: - return None diff --git a/utils/Scalegif.py b/utils/Scalegif.py deleted file mode 100644 index cd5fd77..0000000 --- a/utils/Scalegif.py +++ /dev/null @@ -1,45 +0,0 @@ -from PIL import Image - -def scale_gif(path, scale, new_path=None): - gif = Image.open(path) - if not new_path: - new_path = path - if path[-3:] == "gif": - old_gif_information = { - 'loop': bool(gif.info.get('loop', 1)), - 'duration': gif.info.get('duration', 40), - 'background': gif.info.get('background', 223), - 'extension': gif.info.get('extension', (b'NETSCAPE2.0')), - 'transparency': gif.info.get('transparency', 223) - } - new_frames = get_new_frames(gif, scale) - save_new_gif(new_frames, old_gif_information, new_path) - else: - gif = gif.resize(scale) - gif.save(path) - - -def get_new_frames(gif, scale): - new_frames = [] - actual_frames = gif.n_frames - for frame in range(actual_frames): - gif.seek(frame) - new_frame = Image.new('RGBA', gif.size) - new_frame.paste(gif) - new_frame = new_frame.resize(scale, Image.ANTIALIAS) - new_frames.append(new_frame) - return new_frames - -def save_new_gif(new_frames, old_gif_information, new_path): - new_frames[0].save(new_path, - save_all = True, - append_images = new_frames[1:], - duration = old_gif_information['duration'], - loop = old_gif_information['loop'], - background = old_gif_information['background'], - extension = old_gif_information['extension'] , - transparency = old_gif_information['transparency']) - - -if __name__ == "__main__": - scale_gif(f"Post-qtehpj.gif", (720,1280),"test.gif") \ No newline at end of file diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..eb89ca3 --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1,62 @@ +import os +from pprint import pprint + +from environs import Env + +from RedditDownloader import RedditBot + + +class Scales: + """Some known format are already defined here. You can use them by importing Scales from utils. + + """ + Default = None + + # youtube format + YoutubeShortsFullscreen = (1080, 1920) + YoutubeShortsSquare = (1080, 1080) + YoutubeVideo = (1920, 1080) + + # tiktok format + TikTok = YoutubeShortsFullscreen + + # instagram format + InstagramPhotoSquare = YoutubeShortsSquare + InstagramPhotoLandscape = (1080, 608) + InstagramPhotoPortrait = (1080, 1350) + InstagramStories = YoutubeShortsFullscreen + InstagramReels = InstagramStories + InstagramIGTVCoverPhoto = (420, 654) + InstagramVideoSquare = InstagramPhotoSquare + InstagramVideoLandscape = (1080, 608) + InstagramVideoPortrait = InstagramPhotoPortrait + + # snapchat format + Snapchat = YoutubeShortsFullscreen + + @staticmethod + def show_attributes(): + pprint([i for i in dir(Scales) if not i.startswith("__") and i != "show_attributes"]) + + +class SocialMedias: + YouTube = 0 + TikTok = 1 + Instagram = 2 + Snapchat = 3 + + @staticmethod + def show_attributes(): + pprint([i for i in dir(SocialMedias) if not i.startswith("__") and i != "show_attributes"]) + + +def get_credentials(): + env = Env() + env.read_env() + return env + + +def initialize(base_path: str = os.getcwd()): + RedditBot(get_credentials(), base_path) + print("Application initialized !") + exit() diff --git a/utils/upload_video.py b/utils/upload_video.py deleted file mode 100644 index 624254e..0000000 --- a/utils/upload_video.py +++ /dev/null @@ -1,151 +0,0 @@ -import httplib2 -import os -import random -import sys -import time - -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 - - -# 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] -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): - 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): - 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'] - ) - ) - - # 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) - -# This method implements an exponential backoff strategy to resume a -# failed upload. -def resumable_upload(insert_request): - 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']) - 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 upload_video(video_data): - args = argparser.parse_args() - if not os.path.exists(video_data['file']): - exit("Please specify a valid file using the --file= parameter.") - - youtube = get_authenticated_service(args) - try: - initialize_upload(youtube, video_data) - except HttpError as e: - print("An HTTP error %d occurred:\n%s" % (e.resp.status, e.content)) - -if __name__ == '__main__': - video_data = { - "file": "video.mp4", - "title": "Best of memes!", - "description": "#shorts \n Giving you the hottest memes of the day with funny comments!", - "keywords":"meme,reddit", - "privacyStatus":"private" - } - update_video(video_data) \ No newline at end of file From 60e92c8f71dca56a417c82181d8cfd85c759ca77 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Fri, 3 Dec 2021 10:35:53 +0100 Subject: [PATCH 02/11] doc update --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 50fe352..afd6bc9 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,6 @@ reddit.publish_on(SocialMedias.YouTube, ytb_data) ```` ### for publish_on(SocialMedias.YouTube) -#### still not implemented ````json { @@ -227,7 +226,7 @@ reddit.publish_on(SocialMedias.YouTube, ytb_data) "title": "video title", "description": "video description", "keywords": "tag1,tag2,tag3...", - "privacyStatus": "private|public" + "privacyStatus": "private|public|unlisted" } ```` From dd67a77c3cbd0a817eea6ef9713c0abf55353ac7 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Fri, 3 Dec 2021 10:55:17 +0100 Subject: [PATCH 03/11] Exception name --- RedditDownloader/RedditBot.py | 15 +++++++-------- RedditDownloaderExceptions/RedditBotExceptions.py | 4 ++-- .../YoutubePublisherExceptions.py | 2 +- RedditDownloaderExceptions/__init__.py | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/RedditDownloader/RedditBot.py b/RedditDownloader/RedditBot.py index fcd782a..4542eb7 100644 --- a/RedditDownloader/RedditBot.py +++ b/RedditDownloader/RedditBot.py @@ -6,12 +6,12 @@ import praw import requests from environs import Env +from prawcore import ResponseException from MovieMaker import CreateMovie from Publishers import YtbPublisher -from RedditDownloaderExceptions import MissingCredentialsException, IncorrectCredentialsException +from RedditDownloaderExceptions import MissingRedditCredentialsException, IncorrectRedditCredentialsException from .ScaleImages import Scale -from prawcore import ResponseException class RedditBot(Scale, CreateMovie, YtbPublisher): @@ -32,11 +32,11 @@ def __init__(self, env: Env, base_path=os.getcwd(), log: bool = False) -> None: # Check if credentials exists if not env("REDDIT_CLIENT_ID"): - raise MissingCredentialsException("REDDIT_CLIENT_ID") + raise MissingRedditCredentialsException("REDDIT_CLIENT_ID") if not env("REDDIT_CLIENT_SECRET"): - raise MissingCredentialsException("REDDIT_CLIENT_SECRET") + raise MissingRedditCredentialsException("REDDIT_CLIENT_SECRET") if not env("REDDIT_USER_AGENT"): - raise MissingCredentialsException("REDDIT_USER_AGENT") + raise MissingRedditCredentialsException("REDDIT_USER_AGENT") # connect to reddit self.__reddit = praw.Reddit( @@ -48,8 +48,7 @@ def __init__(self, env: Env, base_path=os.getcwd(), log: bool = False) -> None: try: self.__reddit.user.me() except ResponseException: - raise IncorrectCredentialsException() - + raise IncorrectRedditCredentialsException() # define image format that we want to query self.__accepted_format = ["jpg", "png", "gif"] @@ -77,7 +76,7 @@ def __init__(self, env: Env, base_path=os.getcwd(), log: bool = False) -> None: encoding="utf-8-sig") as f: try: self.__already_downloaded = json.loads(f.read()) - except json.decoder.JSONDecodeError as e: + except json.decoder.JSONDecodeError: self.__already_downloaded = [] def __create_subreddit_folder(self, subreddit: str) -> str: diff --git a/RedditDownloaderExceptions/RedditBotExceptions.py b/RedditDownloaderExceptions/RedditBotExceptions.py index 7472e14..801db5c 100644 --- a/RedditDownloaderExceptions/RedditBotExceptions.py +++ b/RedditDownloaderExceptions/RedditBotExceptions.py @@ -6,7 +6,7 @@ def __init__(self, *args): Exception.__init__(self, *args) -class MissingCredentialsException(RedditBotException): +class MissingRedditCredentialsException(RedditBotException): """Raised when credentials for Reddit are missing """ @@ -19,7 +19,7 @@ def __str__(self): return f"{self.__message} : \"{self.__credential}\"" -class IncorrectCredentialsException(RedditBotException): +class IncorrectRedditCredentialsException(RedditBotException): """Raised when reddit credentials are not correct """ diff --git a/RedditDownloaderExceptions/YoutubePublisherExceptions.py b/RedditDownloaderExceptions/YoutubePublisherExceptions.py index bb5c247..3e1e0bd 100644 --- a/RedditDownloaderExceptions/YoutubePublisherExceptions.py +++ b/RedditDownloaderExceptions/YoutubePublisherExceptions.py @@ -6,7 +6,7 @@ def __init__(self, *args): Exception.__init__(self, *args) -class MissingCredentialsException(YoutubePublisherException): +class MissingYouTubeCredentialsException(YoutubePublisherException): """Raised when credentials for YouTube are missing """ diff --git a/RedditDownloaderExceptions/__init__.py b/RedditDownloaderExceptions/__init__.py index 93e19c9..ba7c9b8 100644 --- a/RedditDownloaderExceptions/__init__.py +++ b/RedditDownloaderExceptions/__init__.py @@ -1,3 +1,3 @@ -from .RedditBotExceptions import MissingCredentialsException, IncorrectCredentialsException -from .YoutubePublisherExceptions import MissingCredentialsException +from .RedditBotExceptions import MissingRedditCredentialsException, IncorrectRedditCredentialsException +from .YoutubePublisherExceptions import MissingYouTubeCredentialsException from .MovieMakerExceptions import MissingImageMagickBinariesException, MissingMP3FilesInMusicsDir From 4e9867508084cb9b2a76282227a4340f02384a79 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Fri, 3 Dec 2021 10:59:19 +0100 Subject: [PATCH 04/11] asdf --- main.py-oauth2.json | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 main.py-oauth2.json diff --git a/main.py-oauth2.json b/main.py-oauth2.json deleted file mode 100644 index 05ccb67..0000000 --- a/main.py-oauth2.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "access_token": "ya29.a0ARrdaM8CIPs-qRP0PvyOUA-_GvFVYRzj3NpppdKndviTwDkVRanLUY_6JQDNnJlaJkdWrESCbE2N_6hOxiXBIze11IKxIb6cqnDWDFgJRM-cw6Al8qHUb68m0tQjEb0c9b8jZhj3SqdMmLRsLZzlUNmC-MUy", - "client_id": "826731140207-vhi85d2ejau1vsbieh2rc3ooki2aggq6.apps.googleusercontent.com", - "client_secret": "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", - "refresh_token": "1//09G5Ur8xWhnSWCgYIARAAGAkSNwF-L9IrvvqdALHOYgzYes_FNUMuU_iXDG2abH-uG76mD-WKG9m1WXFkYuKAeW3WCOsRwWB_sWE", - "token_expiry": "2021-12-02T21:47:50Z", - "token_uri": "https://accounts.google.com/o/oauth2/token", - "user_agent": null, - "revoke_uri": "https://oauth2.googleapis.com/revoke", - "id_token": null, - "id_token_jwt": null, - "token_response": { - "access_token": "ya29.a0ARrdaM8CIPs-qRP0PvyOUA-_GvFVYRzj3NpppdKndviTwDkVRanLUY_6JQDNnJlaJkdWrESCbE2N_6hOxiXBIze11IKxIb6cqnDWDFgJRM-cw6Al8qHUb68m0tQjEb0c9b8jZhj3SqdMmLRsLZzlUNmC-MUy", - "expires_in": 3599, - "refresh_token": "1//09G5Ur8xWhnSWCgYIARAAGAkSNwF-L9IrvvqdALHOYgzYes_FNUMuU_iXDG2abH-uG76mD-WKG9m1WXFkYuKAeW3WCOsRwWB_sWE", - "scope": "https://www.googleapis.com/auth/youtube.upload", - "token_type": "Bearer" - }, - "scopes": [ - "https://www.googleapis.com/auth/youtube.upload" - ], - "token_info_uri": "https://oauth2.googleapis.com/tokeninfo", - "invalid": false, - "_class": "OAuth2Credentials", - "_module": "oauth2client.client" -} \ No newline at end of file From 4cee5a9a47db90ecde270fdab5d320f189850a1a Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Fri, 3 Dec 2021 11:07:18 +0100 Subject: [PATCH 05/11] correct check of musics --- .gitignore | 1 + MovieMaker/MovieMaker.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0c5b0c6..a235a35 100644 --- a/.gitignore +++ b/.gitignore @@ -239,4 +239,5 @@ data/ moviepy-master/ videos/ dump.txt +main.py-oauth2.json # End of https://www.toptal.com/developers/gitignore/api/pycharm+all,python diff --git a/MovieMaker/MovieMaker.py b/MovieMaker/MovieMaker.py index bfaa403..6ada8d8 100644 --- a/MovieMaker/MovieMaker.py +++ b/MovieMaker/MovieMaker.py @@ -35,7 +35,7 @@ def _create_video(self, submission_data: List[dict]) -> str: raise MissingImageMagickBinariesException(self.__env("IMAGEMAGICK_BINARY")) # check if there's music inside the musics dir - if not list(Path(self.__music_path).rglob(".mp3")): + if not list(Path(self.__music_path).rglob("*.mp3")): raise MissingMP3FilesInMusicsDir(self.__music_path) clips = [] From 0539ba820ba64c4a2057049b6c484726c6503675 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Thu, 9 Dec 2021 11:38:49 +0100 Subject: [PATCH 06/11] exception raised when reddit credentials incorrect --- RedditDownloader/RedditBot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/RedditDownloader/RedditBot.py b/RedditDownloader/RedditBot.py index 4542eb7..c9503c4 100644 --- a/RedditDownloader/RedditBot.py +++ b/RedditDownloader/RedditBot.py @@ -45,9 +45,7 @@ def __init__(self, env: Env, base_path=os.getcwd(), log: bool = False) -> None: user_agent=env("REDDIT_USER_AGENT") ) - try: - self.__reddit.user.me() - except ResponseException: + if not self.__reddit.user.me(): raise IncorrectRedditCredentialsException() # define image format that we want to query From 9712679df6b08884e518cc010ad10e549c456be5 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Thu, 9 Dec 2021 11:40:34 +0100 Subject: [PATCH 07/11] exception raised when reddit credentials incorrect --- RedditDownloader/RedditBot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RedditDownloader/RedditBot.py b/RedditDownloader/RedditBot.py index c9503c4..4542eb7 100644 --- a/RedditDownloader/RedditBot.py +++ b/RedditDownloader/RedditBot.py @@ -45,7 +45,9 @@ def __init__(self, env: Env, base_path=os.getcwd(), log: bool = False) -> None: user_agent=env("REDDIT_USER_AGENT") ) - if not self.__reddit.user.me(): + try: + self.__reddit.user.me() + except ResponseException: raise IncorrectRedditCredentialsException() # define image format that we want to query From 3b1dc9a961baab2f9f2e4e18ceb740c0aacef2f2 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Thu, 9 Dec 2021 11:47:25 +0100 Subject: [PATCH 08/11] exception raised when reddit credentials incorrect --- RedditDownloader/RedditBot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RedditDownloader/RedditBot.py b/RedditDownloader/RedditBot.py index 4542eb7..61acb53 100644 --- a/RedditDownloader/RedditBot.py +++ b/RedditDownloader/RedditBot.py @@ -46,7 +46,7 @@ def __init__(self, env: Env, base_path=os.getcwd(), log: bool = False) -> None: ) try: - self.__reddit.user.me() + next(self.__reddit.subreddit("memes").top("day", limit=1)) except ResponseException: raise IncorrectRedditCredentialsException() From 11f6f49285620ec7fc231261157dd1e2399ab6d1 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Thu, 9 Dec 2021 11:54:06 +0100 Subject: [PATCH 09/11] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index afd6bc9..a2eadbe 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You can follow this tutorial to create your application : https://youtu.be/bMT9Z - REDDIT_CLIENT_ID="YourClientId" - REDDIT_USER_AGENT="" - IMAGEMAGICK_BINARIES="C:\Program Files\ImageMagick-7.1.0-Q16-HDRI\magick.exe" (Change the path to your - installation to the convert.exe file. Check [moviepy on pypi](https://pypi.org/project/moviepy/) for more + installation to the magick.exe file. Check [moviepy on pypi](https://pypi.org/project/moviepy/) for more information) The `IMAGEMAGICK_BINARIES` environment variable is only needed if you're on Windows or on Ubuntu 16.04LTS From 2376d50e76bb51d643f64d427f197aeeb08e3e33 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Thu, 9 Dec 2021 11:55:13 +0100 Subject: [PATCH 10/11] fix readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a2eadbe..4b2febc 100644 --- a/README.md +++ b/README.md @@ -53,9 +53,9 @@ video_path = reddit.create_video(data) ytb_data = { "file": video_path, - "title": "#short \n Memes but this time you laugh for real", + "title": "#shorts \n Memes but this time you laugh for real", "description": "why tho", - "keywords": "meme,memes,laugh,internet,short", + "keywords": "meme,memes,laugh,internet,shorts", "privacyStatus": "public" } From 8ed2f832da094b23a2a64fda82109146277fe6f5 Mon Sep 17 00:00:00 2001 From: Julien Gunther Date: Mon, 30 Jan 2023 09:46:44 +0100 Subject: [PATCH 11/11] rename requirements + add pip freeze --- requierments.txt | 1 + requirements-pip-freeze.txt | Bin 0 -> 2242 bytes 2 files changed, 1 insertion(+) create mode 100644 requirements-pip-freeze.txt diff --git a/requierments.txt b/requierments.txt index 9be551e..d549dec 100644 --- a/requierments.txt +++ b/requierments.txt @@ -2,3 +2,4 @@ praw~=7.5.0 environs~=9.3.5 pillow~=8.4.0 moviepy~=1.0.3 +google-api-python-client~=2.31.0 diff --git a/requirements-pip-freeze.txt b/requirements-pip-freeze.txt new file mode 100644 index 0000000000000000000000000000000000000000..d3c6efcc70ec362626c45a84841f10b20023f895 GIT binary patch literal 2242 zcmai#?{3;a5XA3ur9O(QnEY!#V`%&M$TLUEg|i-U{I zgD|T;d(jtd+^>)05AsqcHQ>T+>L;%=x~p^v(AklIgY)z|}|F$?R!-Ky#)dBVt9$K)E9iC#)iGlFN~ zO5J#Er95wu#Y$(^J88O=9@Ml^_uX~om(IKAd&R)B-1j`$@ue@bckZD#M(22uR+QN|dn{cun*hzo3BwFUzje0;UyV#D)9X;trR*L<|3$#c-_1AB$ z{efn7=}Eu)?Pg}Sy~df1+42Grhu7JImqR7i>NY(2q8tz6*7W&!r>6TEJT$iZa2Kd# K