diff --git a/src/engines/UploadEngine/BaseUploadEngine.py b/src/engines/UploadEngine/BaseUploadEngine.py new file mode 100644 index 0000000..479ccb9 --- /dev/null +++ b/src/engines/UploadEngine/BaseUploadEngine.py @@ -0,0 +1,12 @@ +from abc import abstractmethod + +from .. import BaseEngine + + +class BaseUploadEngine(BaseEngine): + def __init__(self, **kwargs) -> None: + ... + + @abstractmethod + def upload(self): + ... diff --git a/src/engines/UploadEngine/TikTokUploadEngine.py b/src/engines/UploadEngine/TikTokUploadEngine.py new file mode 100644 index 0000000..135e161 --- /dev/null +++ b/src/engines/UploadEngine/TikTokUploadEngine.py @@ -0,0 +1,75 @@ +import gradio as gr + +from tiktok_uploader.upload import upload_video + +from .BaseUploadEngine import BaseUploadEngine + + +class TikTokUploadEngine(BaseUploadEngine): + name = "TikTokUpload" + description = "Upload to TikTok" + + num_options = 1 + + def __init__(self, options) -> None: + self.hashtags = options[0] + super().__init__() + + def upload(self): + cookies = self.get_setting(type="cookies")["cookies"] + if cookies == None: + gr.Warning( + "Skipping upload to TikTok because no cookies were provided. Please provide cookies in the settings." + ) + return + title: str = self.ctx.title + description: str = self.ctx.description + hastags = self.hashtags.strip().split(" ") + + # extract any hashtags from the description / title and remove them from the description + for word in title.split(): + if word.startswith("#"): + hastags.append(word) + title = title.replace(word, "") + for word in description.split(): + if word.startswith("#"): + hastags.append(word) + description = description.replace(word, "") + + title = title.strip() + description = description.strip() + hastags_str = " ".join(hastags) + " " if hastags else "" + failed = upload_video( + filename=self.ctx.get_file_path("final.mp4"), + description=f"{title} {description} {hastags_str}", + cookies_str=cookies, + browser="firefox", + ) + for _ in failed: + gr.Error(f"Failed to upload to TikTok") + + @classmethod + def get_options(cls): + hashtags = gr.Textbox(label="hashtags", type="text", value="#fyp #foryou") + return [hashtags] + + @classmethod + def get_settings(cls): + current_settings = cls.get_setting(type="cookies") or {"cookies": ""} + gr.Markdown( + "Input your TikTok session cookies. You can get them as shown [here](https://github.com/wkaisertexas/tiktok-uploader?tab=readme-ov-file#authentication)." + ) + cookies_input = gr.Textbox( + lines=20, + max_lines=50, + label="cookies", + type="text", + value=current_settings["cookies"], + ) + cookies_save_btn = gr.Button("Save") + + def save(cookies: str): + cls.store_setting(type="cookies", data={"cookies": cookies}) + gr.Info("Cookies saved successfully") + + cookies_save_btn.click(save, inputs=[cookies_input]) diff --git a/src/engines/UploadEngine/YouTubeUploadEngine.py b/src/engines/UploadEngine/YouTubeUploadEngine.py new file mode 100644 index 0000000..b773c7e --- /dev/null +++ b/src/engines/UploadEngine/YouTubeUploadEngine.py @@ -0,0 +1,115 @@ +import gradio as gr +import orjson + +from google_auth_oauthlib.flow import InstalledAppFlow + +from . import BaseUploadEngine +from ...utils import youtube_uploading + +class YouTubeUploadEngine(BaseUploadEngine): + name = "YouTube" + description = "Upload videos to YouTube" + + num_options = 2 + + def __init__(self, options: list): + self.oauth_name = options[0] + self.oauth = self.retrieve_setting(type="oauth_credentials")[self.oauth_name] + self.credentials = self.retrieve_setting(type="youtube_client_secrets")[self.oauth["client_secret"]] + + self.hashtags = options[1] + + @classmethod + def __oauth(cls, credentials): + flow = InstalledAppFlow.from_client_config( + credentials, scopes=["https://www.googleapis.com/auth/youtube.upload"] + ) + user_credentials = flow.run_local_server( + success_message="Heyy, yippie, you're authenticated ! You can close this window now !", + authorization_prompt_message="Please authorize this app to upload videos on your YouTube account !", + ) + + result = user_credentials.to_json() + if isinstance(result, str): + result = orjson.loads(result) + return result + + def upload(self): + options = { + "file": self.ctx.get_file_path("final.mp4"), + "title": self.ctx.title + " | " + self.hashtags, + "description": self.ctx.description, + "privacyStatus": "private", + "category": 28, + } + try: + youtube_uploading.upload(self.oauth["credentials"], options) + except Exception as e: + #this means we need to re-authenticate likely + # use self.__oauth to re-authenticate + new_oauth = self.__oauth(self.credentials) + #also update the credentials in the settings + current_oauths = self.retrieve_setting(type="oauth_credentials") or {} + current_oauths[self.oauth_name] = { + "client_secret": self.oauth["client_secret"], + "credentials": new_oauth + } + self.store_setting( + type="oauth_credentials", + data=current_oauths, + ) + self.oauth = current_oauths[self.oauth_name] + youtube_uploading.upload(self.oauth["credentials"], options) + + @classmethod + def get_options(cls): + choices = cls.retrieve_setting(type="oauth_credentials") or {} + choices = list(choices.keys()) + return [ + gr.Dropdown( + choices=choices, label="Choose Channel", value=choices[0] if choices else "No channels available !" + ), + gr.Textbox(label="Hashtags", value="#shorts", max_lines=1), + ] + + @classmethod + def get_settings(cls): + with gr.Row(): + with gr.Column() as ytb_secret: + clien_secret_name = gr.Textbox(label="Name", max_lines=1) + client_secret_file = gr.File( + label="Client Secret File", file_types=["json"], type="binary" + ) + submit_button = gr.Button("Save") + def save(binary, clien_secret_name): + current_client_secrets = cls.retrieve_setting(type="youtube_client_secrets") or {} + client_secret_json = orjson.loads(binary) + current_client_secrets[clien_secret_name] = client_secret_json + cls.store_setting( + type="youtube_client_secrets", + data=current_client_secrets, + ) + gr.Info(f"{clien_secret_name} saved successfully !") + + submit_button.click(save, inputs=[client_secret_file, clien_secret_name]) + + with gr.Column() as ytb_oauth: + possible_client_secrets = cls.retrieve_setting(type="youtube_client_secrets") or {} + possible_client_secrets = list(possible_client_secrets.keys()) + choosen_client_secret = gr.Dropdown(label="Login secret", choices=possible_client_secrets) + name = gr.Textbox(label="Name", max_lines=1) + login_button = gr.Button("Login", variant="primary") + def login(choosen_client_secret, name): + choosen_secret_data = cls.retrieve_setting(type="youtube_client_secrets")[choosen_client_secret] + new_oauth_entry = cls.__oauth(choosen_secret_data) + current_oauths = cls.retrieve_setting(type="oauth_credentials") or {} + current_oauths[name] = { + "client_secret": choosen_client_secret, + "credentials": new_oauth_entry + } + cls.store_setting( + type="oauth_credentials", + data=current_oauths, + ) + gr.Info(f"{name} saved successfully !") + login_button.click(login, inputs=[choosen_client_secret, name]) diff --git a/src/engines/UploadEngine/__init__.py b/src/engines/UploadEngine/__init__.py new file mode 100644 index 0000000..7c64349 --- /dev/null +++ b/src/engines/UploadEngine/__init__.py @@ -0,0 +1,3 @@ +from .BaseUploadEngine import BaseUploadEngine +from .TikTokUploadEngine import TikTokUploadEngine +from .YouTubeUploadEngine import YouTubeUploadEngine \ No newline at end of file diff --git a/src/engines/__init__.py b/src/engines/__init__.py index d1808e7..725b566 100644 --- a/src/engines/__init__.py +++ b/src/engines/__init__.py @@ -9,6 +9,7 @@ from . import AssetsEngine from . import SettingsEngine from . import BackgroundEngine from . import MetadataEngine +from . import UploadEngine class EngineDict(TypedDict): @@ -38,7 +39,7 @@ ENGINES: dict[str, EngineDict] = { "multiple": False, }, "TTSEngine": { - "classes": [TTSEngine.CoquiTTSEngine, TTSEngine.ElevenLabsTTSEngine], + "classes": [TTSEngine.CoquiTTSEngine], "multiple": False, }, "CaptioningEngine": { @@ -46,7 +47,11 @@ ENGINES: dict[str, EngineDict] = { "multiple": False, }, "AssetsEngine": { - "classes": [AssetsEngine.DallEAssetsEngine, NoneEngine], + "classes": [ + AssetsEngine.DallEAssetsEngine, + AssetsEngine.GoogleAssetsEngine, + NoneEngine, + ], "multiple": True, }, "BackgroundEngine": { @@ -57,4 +62,8 @@ ENGINES: dict[str, EngineDict] = { "classes": [MetadataEngine.ShortsMetadataEngine], "multiple": False, }, + "UploadEngine": { + "classes": [UploadEngine.TikTokUploadEngine, UploadEngine.YouTubeUploadEngine, NoneEngine], + "multiple": True, + }, }