Skip to content

Commit

Permalink
Merge pull request #798 from Wolfy76700/feature/bsky-post
Browse files Browse the repository at this point in the history
Added Bluesky posting feature
  • Loading branch information
joaorb64 authored Jan 9, 2025
2 parents 023cf08 + 75545eb commit 7aa022c
Show file tree
Hide file tree
Showing 18 changed files with 1,131 additions and 306 deletions.
Binary file modified TSH.exe
Binary file not shown.
4 changes: 4 additions & 0 deletions assets/icons/bsky.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion dependencies/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ cloudscraper==1.2.71
loguru==0.7.3
flask-socketio
simple_websocket
atproto==0.0.56
termcolor==2.5.0
orjson==3.10.13
msgpack==1.1.0
qasync==0.27.1
tzdata
tzdata
41 changes: 37 additions & 4 deletions src/Helpers/TSHAltTextHelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from qtpy.QtGui import *
from qtpy.QtWidgets import *
from qtpy.QtCore import *
from atproto import client_utils

def add_alt_text_tooltip_to_button(push_button: QPushButton):
altTextTooltip = QApplication.translate(
Expand All @@ -19,7 +20,33 @@ def load_program_state():
return (data_json)


def generate_youtube(scoreboard_id=1, use_phase_name=True):
def generate_bsky_text(scoreboard_id=1, use_phase_name=True):
def transform_yt_into_bsky(description, data):
text = "\n".join(description.split("\n")[:-1]).strip("\n")
text = "🔴 " + QApplication.translate("altText", "LIVE NOW") + "\n\n" + text

link_text = QApplication.translate("altText", "Click here to watch")
link_url = data.get("score").get(str(scoreboard_id)).get("stream_url")
if link_url:
text += "\n\n"
result = client_utils.TextBuilder().text(text).link(link_text, link_url)
raw_text = text + link_text
else:
result = client_utils.TextBuilder().text(text)
raw_text = text
return(raw_text, result)

post_length_limit = 300
data = load_program_state()
title, description = generate_youtube(scoreboard_id, use_phase_name)
raw_text, builder = transform_yt_into_bsky(description, data)
if len(raw_text) > post_length_limit:
title, description = generate_youtube(scoreboard_id, use_phase_name, use_characters=False)
raw_text, builder = transform_yt_into_bsky(description, data)
return(raw_text, builder)


def generate_youtube(scoreboard_id=1, use_phase_name=True, use_characters=True):
title_length_limit = 100 # Character limit for video titles
data = load_program_state()
tournament_name = data.get("tournamentInfo").get("tournamentName")
Expand Down Expand Up @@ -83,8 +110,12 @@ def generate_youtube(scoreboard_id=1, use_phase_name=True):
title_long = f'{title_long}' + \
" / ".join(player_names_with_characters)
title_short = f'{title_short}' + " / ".join(player_names)
description = description + \
" / ".join(player_names_with_characters)
if use_characters:
description = description + \
" / ".join(player_names_with_characters)
else:
description = description + \
" / ".join(player_names)

if team_id == "1":
title_long = f'{title_long} ' + \
Expand All @@ -110,7 +141,7 @@ def generate_youtube(scoreboard_id=1, use_phase_name=True):
" " + "https://github.com/joaorb64/TournamentStreamHelper/releases"
description = description.strip()

if len(title_long) <= title_length_limit:
if len(title_long) <= title_length_limit and use_characters:
return (title_long, description)
else:
if len(title_short) > title_length_limit:
Expand Down Expand Up @@ -226,6 +257,8 @@ def CalculatePlacement(x, bracket_type="DOUBLE_ELIMINATION"):
print(colored("TEST MODE - TSHAltTextHelper.py", "red"))
print("====")
title, description = generate_youtube(1)
raw_bsky, builder = generate_bsky_text(1)
print(colored("YouTube title: ", "yellow") + f"{title}")
print(colored(f"\nDescription: \n", "yellow") + f"{description}")
print(colored(f"\nTop 8 alt text: \n", "yellow")+f"{generate_top_n_alt_text()}")
print(colored(f"\nBluesky post: \n", "yellow")+f"{raw_bsky}")
28 changes: 28 additions & 0 deletions src/Helpers/TSHBskyHelper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import json
import math
import textwrap
from qtpy.QtGui import *
from qtpy.QtWidgets import *
from qtpy.QtCore import *
from .TSHAltTextHelper import generate_bsky_text, generate_youtube
from .TSHLocaleHelper import TSHLocaleHelper
from src.SettingsManager import SettingsManager
from atproto import Client
from atproto_client.models.app.bsky.embed.defs import AspectRatio

def post_to_bsky(scoreboardNumber=1, image_path=None):
locale = TSHLocaleHelper.programLocale
bsky_account = SettingsManager.Get("bsky_account", {})
if not bsky_account or not bsky_account.get("username") or not bsky_account.get("host") or not bsky_account.get("app_password"):
raise ValueError(QApplication.translate("app", "Bluesky account not correctly set"))
client = Client(bsky_account.get("host", "https://bsky.social"))
client.login(bsky_account["username"].lstrip("@"), bsky_account["app_password"])

raw_text, builder = generate_bsky_text(scoreboard_id = scoreboardNumber)
yt_title, yt_description = generate_youtube(scoreboard_id = scoreboardNumber) # Use YouTube Description as Alt Text

pixmap = QPixmap(image_path, "RGBA")
aspect_ratio = AspectRatio(width=pixmap.width(), height=pixmap.height())

with open(image_path, "rb") as image:
post = client.send_images(builder, images=[image.read()], image_alts=[yt_description], langs=[locale], image_aspect_ratios=[aspect_ratio])
20 changes: 19 additions & 1 deletion src/Settings/SettingsWidget.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from qtpy.QtWidgets import *
from ..TSHHotkeys import TSHHotkeys
from ..SettingsManager import SettingsManager
import textwrap


class SettingsWidget(QWidget):
Expand All @@ -19,7 +20,7 @@ def __init__(self, settingsBase="", settings=[]):
for setting in settings:
self.AddSetting(*setting)

def AddSetting(self, name: str, setting: str, type: str, defaultValue, callback=lambda: None):
def AddSetting(self, name: str, setting: str, type: str, defaultValue, callback=lambda: None, tooltip=None):
lastRow = self.layout().rowCount()

self.layout().addWidget(QLabel(name), lastRow, 0)
Expand Down Expand Up @@ -59,6 +60,23 @@ def AddSetting(self, name: str, setting: str, type: str, defaultValue, callback=
callback()
]
)
elif type == "textbox" or type == "password":
settingWidget = QLineEdit()
if type == "password":
settingWidget.setEchoMode(QLineEdit.Password)
settingWidget.textChanged.connect(
lambda val=None: SettingsManager.Set(self.settingsBase+"."+setting, settingWidget.text()))
settingWidget.setText(SettingsManager.Get(
self.settingsBase+"."+setting, defaultValue))
resetButton.clicked.connect(
lambda bt=None, setting=setting, settingWidget=settingWidget: [
settingWidget.setText(defaultValue),
callback()
]
)

if tooltip:
settingWidget.setToolTip('\n'.join(textwrap.wrap(tooltip, 40)))

self.layout().addWidget(settingWidget, lastRow, 1)
self.layout().addWidget(resetButton, lastRow, 2)
34 changes: 34 additions & 0 deletions src/Settings/TSHSettingsWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,40 @@ def UiMounted(self):

self.add_setting_widget(QApplication.translate(
"settings", "Hotkeys"), SettingsWidget("hotkeys", hotkeySettings))

# Add Bluesky settings
bskySettings = []
bskySettings.append((
QApplication.translate(
"settings.bsky", "Host server"),
"host",
"textbox",
"https://bsky.social"
))
bskySettings.append((
QApplication.translate(
"settings.bsky", "Bluesky Handle"),
"username",
"textbox",
""
))
bskySettings.append((
QApplication.translate(
"settings.bsky", "Application Password"),
"app_password",
"password",
"",
None,
QApplication.translate(
"settings.bsky", "You can get an app password by going into your Bluesky settings -> Privacy & Security") + "\n" +
QApplication.translate(
"settings.bsky", "Please note that said app password will be stored in plain text on your computer") + "\n\n" +
QApplication.translate(
"settings.bsky", "Do not use your regular account password!").upper()
))

self.add_setting_widget(QApplication.translate(
"settings", "Bluesky"), SettingsWidget("bsky_account", bskySettings))

self.resize(1000, 500)
QApplication.processEvents()
Expand Down
65 changes: 53 additions & 12 deletions src/TSHScoreboardWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import List
from src.TSHColorButton import TSHColorButton
from .Helpers.TSHDirHelper import TSHResolve
from .Helpers.TSHBskyHelper import post_to_bsky

from src.TSHSelectSetWindow import TSHSelectSetWindow
from src.TSHSelectStationWindow import TSHSelectStationWindow
Expand Down Expand Up @@ -190,6 +191,13 @@ def __init__(self, scoreboardNumber=1, *args):
col.layout().addWidget(self.thumbnailBtn, Qt.AlignmentFlag.AlignRight)
# self.thumbnailBtn.setPopupMode(QToolButton.InstantPopup)
self.thumbnailBtn.clicked.connect(self.GenerateThumbnail)

self.bskyBtn = QPushButton(
QApplication.translate("app", "Post to Bluesky") + " ")
self.bskyBtn.setIcon(QIcon('assets/icons/bsky.svg'))
self.bskyBtn.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Fixed)
col.layout().addWidget(self.bskyBtn, Qt.AlignmentFlag.AlignRight)
self.bskyBtn.clicked.connect(self.PostToBsky)

# VISIBILITY
col = QWidget()
Expand Down Expand Up @@ -246,6 +254,16 @@ def __init__(self, scoreboardNumber=1, *args):

self.innerWidget.layout().addWidget(bottomOptions)

self.streamUrl = QHBoxLayout()
self.streamUrlLabel = QLabel(QApplication.translate("app", "Stream URL") + " ")
self.streamUrl.layout().addWidget(self.streamUrlLabel)
self.streamUrlTextBox = QLineEdit()
self.streamUrl.layout().addWidget(self.streamUrlTextBox)
self.streamUrlTextBox.textChanged.connect(
lambda value=None: StateManager.Set(
f"score.{self.scoreboardNumber}.stream_url", value))
bottomOptions.layout().addLayout(self.streamUrl)

self.btSelectSet = QPushButton(
QApplication.translate("app", "Load set"))
self.btSelectSet.setIcon(QIcon("./assets/icons/list.svg"))
Expand Down Expand Up @@ -504,7 +522,7 @@ def ExportTeamLogo(self, team, value):
StateManager.Set(
f"score.{self.scoreboardNumber}.team.{team}.logo", None)

def GenerateThumbnail(self):
def GenerateThumbnail(self, quiet_mode=False):
msgBox = QMessageBox()
msgBox.setWindowIcon(QIcon('assets/icons/icon.png'))
msgBox.setWindowTitle(QApplication.translate(
Expand All @@ -519,25 +537,48 @@ def GenerateThumbnail(self):
# msgBox.setInformativeText(thumbnailPath)

thumbnail_settings = SettingsManager.Get("thumbnail_config")
if thumbnail_settings.get("open_explorer"):
outThumbDir = f"{os.getcwd()}/out/thumbnails/"
if platform.system() == "Windows":
thumbnailPath = thumbnailPath[2:].replace("/", "\\")
outThumbDir = f"{os.getcwd()}\\{thumbnailPath}"
# os.startfile(outThumbDir)
subprocess.Popen(r'explorer /select,"'+outThumbDir+'"')
elif platform.system() == "Darwin":
subprocess.Popen(["open", outThumbDir])
if not quiet_mode:
if thumbnail_settings.get("open_explorer"):
outThumbDir = f"{os.getcwd()}/out/thumbnails/"
if platform.system() == "Windows":
thumbnailPath = thumbnailPath[2:].replace("/", "\\")
outThumbDir = f"{os.getcwd()}\\{thumbnailPath}"
# os.startfile(outThumbDir)
subprocess.Popen(r'explorer /select,"'+outThumbDir+'"')
elif platform.system() == "Darwin":
subprocess.Popen(["open", outThumbDir])
else:
subprocess.Popen(["xdg-open", outThumbDir])
else:
subprocess.Popen(["xdg-open", outThumbDir])
msgBox.exec()
else:
msgBox.exec()
return(thumbnailPath)
except Exception as e:
msgBox.setText(QApplication.translate("app", "Warning"))
msgBox.setInformativeText(str(e))
msgBox.setIcon(QMessageBox.Warning)
msgBox.exec()

def PostToBsky(self):
thumbnailPath = self.GenerateThumbnail(quiet_mode=True)
if thumbnailPath:
msgBox = QMessageBox()
msgBox.setWindowIcon(QIcon('assets/icons/icon.png'))
msgBox.setWindowTitle(QApplication.translate(
"app", "TSH - Bluesky"))

try:
post_to_bsky(scoreboardNumber=self.scoreboardNumber, image_path=thumbnailPath.replace(".png", ".jpg"))
username = SettingsManager.Get("bsky_account", {}).get("username")
msgBox.setText(QApplication.translate("app", "The post has successfully been sent to account {0}").format(username))
msgBox.setIcon(QMessageBox.NoIcon)
msgBox.exec()
except Exception as e:
msgBox.setText(QApplication.translate("app", "Warning"))
msgBox.setInformativeText(str(e))
msgBox.setIcon(QMessageBox.Warning)
msgBox.exec()

def ToggleElements(self, action: QAction, elements):
for pw in self.playerWidgets:
for element in elements:
Expand Down
Loading

0 comments on commit 7aa022c

Please sign in to comment.