diff --git a/TSH.exe b/TSH.exe index d94b90f28..e943b9232 100644 Binary files a/TSH.exe and b/TSH.exe differ diff --git a/assets/icons/station.svg b/assets/icons/station.svg new file mode 100644 index 000000000..303f79e8a --- /dev/null +++ b/assets/icons/station.svg @@ -0,0 +1,53 @@ + + + + + + + + diff --git a/src/TSHScoreboardWidget.py b/src/TSHScoreboardWidget.py index 0ed00099c..4fbf188e9 100644 --- a/src/TSHScoreboardWidget.py +++ b/src/TSHScoreboardWidget.py @@ -9,6 +9,7 @@ from src.TSHColorButton import TSHColorButton from src.TSHSelectSetWindow import TSHSelectSetWindow +from src.TSHSelectStationWindow import TSHSelectStationWindow from .TSHScoreboardPlayerWidget import TSHScoreboardPlayerWidget from .SettingsManager import SettingsManager @@ -26,6 +27,8 @@ class TSHScoreboardWidgetSignals(QObject): NewSetSelected = Signal(object) SetSelection = Signal() StreamSetSelection = Signal() + StationSelection = Signal() + StationSelected = Signal(object) UserSetSelection = Signal() CommandScoreChange = Signal(int, int) CommandTeamColor = Signal(int, str) @@ -49,7 +52,8 @@ def __init__(self, scoreboardNumber=1, *args): self.signals.UpdateSetData.connect(self.UpdateSetData) self.signals.NewSetSelected.connect(self.NewSetSelected) self.signals.SetSelection.connect(self.LoadSetClicked) - self.signals.StreamSetSelection.connect(self.LoadStreamSetClicked) + self.signals.StationSelected.connect(self.LoadStationSet) + self.signals.StationSelection.connect(self.LoadStationSetClicked) self.signals.UserSetSelection.connect(self.LoadUserSetClicked) self.signals.ChangeSetData.connect(self.ChangeSetData) @@ -82,6 +86,8 @@ def __init__(self, scoreboardNumber=1, *args): self.lastSetSelected = None + self.lastStationSelected = None + self.autoUpdateTimer: QTimer = None self.timeLeftTimer: QTimer = None @@ -203,31 +209,20 @@ def __init__(self, scoreboardNumber=1, *args): hbox = QHBoxLayout() bottomOptions.layout().addLayout(hbox) - self.btLoadStreamSet = QPushButton( - QApplication.translate("app", "Load current stream set")) - self.btLoadStreamSet.setIcon(QIcon("./assets/icons/twitch.svg")) - self.btLoadStreamSet.setEnabled(False) - hbox.addWidget(self.btLoadStreamSet) - self.btLoadStreamSet.clicked.connect( - self.signals.StreamSetSelection.emit) - TSHTournamentDataProvider.instance.signals.twitch_username_updated.connect( - self.UpdateStreamButton) - - self.btLoadStreamSetOptions = QPushButton() - self.btLoadStreamSetOptions.setSizePolicy( - QSizePolicy.Maximum, QSizePolicy.Maximum) - self.btLoadStreamSetOptions.setIcon( - QIcon("./assets/icons/settings.svg")) - self.btLoadStreamSetOptions.clicked.connect( - self.LoadStreamSetOptionsClicked) - hbox.addWidget(self.btLoadStreamSetOptions) + self.btLoadStationSet = QPushButton( + QApplication.translate("app", "Track station sets")) + self.btLoadStationSet.setIcon(QIcon("./assets/icons/station.svg")) + hbox.addWidget(self.btLoadStationSet) + self.btLoadStationSet.clicked.connect( + self.signals.StationSelection.emit) hbox = QHBoxLayout() bottomOptions.layout().addLayout(hbox) if self.scoreboardNumber <= 1: self.btLoadPlayerSet = QPushButton("Load player set") - self.btLoadPlayerSet.setIcon(QIcon("./assets/icons/person_search.svg")) + self.btLoadPlayerSet.setIcon( + QIcon("./assets/icons/person_search.svg")) self.btLoadPlayerSet.setEnabled(False) self.btLoadPlayerSet.clicked.connect( self.signals.UserSetSelection.emit) @@ -251,6 +246,7 @@ def __init__(self, scoreboardNumber=1, *args): TSHTournamentDataProvider.instance.signals.tournament_changed.emit() self.selectSetWindow = TSHSelectSetWindow(self) + self.selectStationWindow = TSHSelectStationWindow(self) self.timerLayout = QWidget() self.timerLayout.setLayout(QHBoxLayout()) @@ -369,7 +365,8 @@ def __init__(self, scoreboardNumber=1, *args): self.scoreColumn.findChild(QSpinBox, "best_of").valueChanged.connect( lambda value: [ - StateManager.Set(f"score.{self.scoreboardNumber}.best_of", value), + StateManager.Set( + f"score.{self.scoreboardNumber}.best_of", value), StateManager.Set(f"score.{self.scoreboardNumber}.best_of_text", TSHLocaleHelper.matchNames.get( "best_of").format(value) if value > 0 else ""), ] @@ -443,16 +440,14 @@ def __init__(self, scoreboardNumber=1, *args): if self.scoreColumn.findChild(QComboBox, "match").findText(matchString) < 0: self.scoreColumn.findChild( QComboBox, "match").addItem(matchString) - - if self.scoreboardNumber > 1: - self.UpdateStreamButton() def ExportTeamLogo(self, team, value): if os.path.exists(f"./user_data/team_logo/{value.lower()}.png"): StateManager.Set(f"score.{self.scoreboardNumber}.team.{team}.logo", f"./user_data/team_logo/{value.lower()}.png") else: - StateManager.Set(f"score.{self.scoreboardNumber}.team.{team}.logo", None) + StateManager.Set( + f"score.{self.scoreboardNumber}.team.{team}.logo", None) def GenerateThumbnail(self): msgBox = QMessageBox() @@ -460,7 +455,8 @@ def GenerateThumbnail(self): msgBox.setWindowTitle(QApplication.translate( "thumb_app", "TSH - Thumbnail")) try: - thumbnailPath = thumbnail.generate(settingsManager=SettingsManager, scoreboardNumber=self.scoreboardNumber) + thumbnailPath = thumbnail.generate( + settingsManager=SettingsManager, scoreboardNumber=self.scoreboardNumber) msgBox.setText(QApplication.translate( "thumb_app", "The thumbnail has been generated here:") + " ") msgBox.setIcon(QMessageBox.NoIcon) @@ -496,7 +492,6 @@ def UpdateBottomButtons(self): self.btSelectSet.setText( QApplication.translate("app", "Load set from {0}").format(TSHTournamentDataProvider.instance.provider.url)) self.btSelectSet.setEnabled(True) - self.btLoadStreamSet.setEnabled(True) if self.scoreboardNumber <= 1: self.btLoadPlayerSet.setEnabled(True) else: @@ -545,7 +540,7 @@ def SetPlayersPerTeam(self, number): path=f'score.{self.scoreboardNumber}.team.{2}.player.{len(self.team2playerWidgets)+1}', scoreboardNumber=self.scoreboardNumber) self.playerWidgets.append(p) - + self.team2column.findChild( QScrollArea).widget().layout().addWidget(p) p.SetCharactersPerPlayer(self.charNumber.value()) @@ -587,7 +582,8 @@ def SetPlayersPerTeam(self, number): if StateManager.Get(f'score.{self.scoreboardNumber}.team.{team}'): for k in list(StateManager.Get(f'score.{self.scoreboardNumber}.team.{team}.player').keys()): if int(k) > number: - StateManager.Unset(f'score.{self.scoreboardNumber}.team.{team}.player.{k}') + StateManager.Unset( + f'score.{self.scoreboardNumber}.team.{team}.player.{k}') if number > 1: self.team1column.findChild(QLineEdit, "teamName").setVisible(True) @@ -648,7 +644,8 @@ def SwapTeams(self): self.teamsSwapped = not self.teamsSwapped finally: - StateManager.Set(f"score.{self.scoreboardNumber}.teamsSwapped", self.teamsSwapped) + StateManager.Set( + f"score.{self.scoreboardNumber}.teamsSwapped", self.teamsSwapped) for p in self.playerWidgets: p.dataLock.release() @@ -677,7 +674,11 @@ def NewSetSelected(self, data): if data.get("auto_update") == "set": self.labelAutoUpdate.setText("Auto update (Set)") elif data.get("auto_update") == "stream": - self.labelAutoUpdate.setText("Auto update (Stream)") + self.labelAutoUpdate.setText( + f"Auto update (Stream [{self.lastStationSelected.get('identifier')}])") + elif data.get("auto_update") == "station": + self.labelAutoUpdate.setText( + f"Auto update (Station [{self.lastStationSelected.get('identifier')}])") elif data.get("auto_update") == "user": self.labelAutoUpdate.setText("Auto update (User)") else: @@ -692,7 +693,8 @@ def NewSetSelected(self, data): TSHTournamentDataProvider.instance.GetStreamQueue() if data.get("id") != None and data.get("id") != self.lastSetSelected: - StateManager.Unset(f'score.{self.scoreboardNumber}.stage_strike') + StateManager.Unset( + f'score.{self.scoreboardNumber}.stage_strike') self.lastSetSelected = data.get("id") self.CommandClearAll() self.ClearScore() @@ -712,9 +714,9 @@ def NewSetSelected(self, data): self.autoUpdateTimer.timeout.connect( lambda setId=data: self.AutoUpdate(data)) - if data.get("auto_update") == "stream": + if data.get("auto_update") in ("stream", "station"): self.autoUpdateTimer.timeout.connect( - lambda setId=data: TSHTournamentDataProvider.instance.LoadStreamSet(self, SettingsManager.Get("twitch_username"))) + lambda setId=data: TSHTournamentDataProvider.instance.LoadStationSet(self)) if data.get("auto_update") == "user": self.autoUpdateTimer.timeout.connect( @@ -743,25 +745,14 @@ def LoadSetClicked(self): self.selectSetWindow.LoadSets() self.selectSetWindow.show() - def LoadStreamSetClicked(self): + def LoadStationSet(self, station): self.lastSetSelected = None - TSHTournamentDataProvider.instance.LoadStreamSet( - self, SettingsManager.Get("twitch_username")) - - def LoadStreamSetOptionsClicked(self): - TSHTournamentDataProvider.instance.SetTwitchUsername(self) - - def UpdateStreamButton(self): - if SettingsManager.Get("twitch_username"): - self.btLoadStreamSet.setText( - QApplication.translate("app", "Load current stream set") + " "+QApplication.translate("punctuation", "(")+SettingsManager.Get("twitch_username")+QApplication.translate("punctuation", ")")) - self.btLoadStreamSet.setEnabled(True) - StateManager.Set(f"currentStream", - SettingsManager.Get("twitch_username")) - else: - self.btLoadStreamSet.setText( - QApplication.translate("app", "Load current stream set")) - self.btLoadStreamSet.setEnabled(False) + self.lastStationSelected = station + TSHTournamentDataProvider.instance.LoadStationSet(self) + + def LoadStationSetClicked(self): + self.selectStationWindow.LoadStations() + self.selectStationWindow.show() def UpdateUserSetButton(self): provider = None diff --git a/src/TSHSelectSetWindow.py b/src/TSHSelectSetWindow.py index e0bc78b68..567d0d98d 100644 --- a/src/TSHSelectSetWindow.py +++ b/src/TSHSelectSetWindow.py @@ -55,7 +55,7 @@ def filterList(text): self.startggSetSelectionItemList.setEditTriggers( QAbstractItemView.NoEditTriggers) self.startggSetSelectionItemList.setModel(self.proxyModel) - self.startggSetSelectionItemList.setColumnHidden(5, True) + self.startggSetSelectionItemList.setColumnHidden(6, True) self.startggSetSelectionItemList.horizontalHeader( ).setSectionResizeMode(QHeaderView.Stretch) self.startggSetSelectionItemList.resizeColumnsToContents() @@ -90,13 +90,15 @@ def LoadSets(self): def SetSets(self, sets): logger.info("Got sets" + str(len(sets))) model = QStandardItemModel() - horizontal_labels = ["Stream", "Wave", "Title", "Player 1", "Player 2"] + horizontal_labels = ["Stream", "Station", + "Wave", "Title", "Player 1", "Player 2"] horizontal_labels[0] = QApplication.translate("app", "Stream") - horizontal_labels[1] = QApplication.translate("app", "Phase") - horizontal_labels[2] = QApplication.translate("app", "Match") - horizontal_labels[3] = QApplication.translate( - "app", "Player {0}").format(1) + horizontal_labels[1] = QApplication.translate("app", "Station") + horizontal_labels[2] = QApplication.translate("app", "Phase") + horizontal_labels[3] = QApplication.translate("app", "Match") horizontal_labels[4] = QApplication.translate( + "app", "Player {0}").format(1) + horizontal_labels[5] = QApplication.translate( "app", "Player {0}").format(2) model.setHorizontalHeaderLabels(horizontal_labels) @@ -124,6 +126,8 @@ def SetSets(self, sets): model.appendRow([ QStandardItem(s.get("stream", "")), + QStandardItem(str(s.get("station", "")) if s.get( + "station", "") != None else ""), QStandardItem(s.get("tournament_phase", "")), QStandardItem(s["round_name"]), QStandardItem(player_names[0]), @@ -132,7 +136,7 @@ def SetSets(self, sets): ]) self.proxyModel.setSourceModel(model) - self.startggSetSelectionItemList.setColumnHidden(5, True) + self.startggSetSelectionItemList.setColumnHidden(6, True) self.startggSetSelectionItemList.resizeColumnsToContents() self.startggSetSelectionItemList.horizontalHeader( ).setSectionResizeMode(QHeaderView.Stretch) @@ -146,7 +150,7 @@ def LoadSelectedSet(self): row = self.startggSetSelectionItemList.selectionModel().selectedRows()[ 0].row() setId = self.startggSetSelectionItemList.model().index( - row, 5).data(Qt.ItemDataRole.UserRole) + row, 6).data(Qt.ItemDataRole.UserRole) self.close() if setId: diff --git a/src/TSHSelectStationWindow.py b/src/TSHSelectStationWindow.py new file mode 100644 index 000000000..004e539b8 --- /dev/null +++ b/src/TSHSelectStationWindow.py @@ -0,0 +1,123 @@ +import traceback +from qtpy.QtGui import * +from qtpy.QtWidgets import * +from qtpy.QtCore import * +from loguru import logger + +from src.TSHTournamentDataProvider import TSHTournamentDataProvider + + +class TSHSelectStationWindow(QDialog): + def __init__(self, parent: QWidget) -> None: + super().__init__(parent) + + self.setWindowTitle( + QApplication.translate("app", "Select a station")) + self.setWindowModality(Qt.WindowModal) + + layout = QVBoxLayout() + self.setLayout(layout) + + self.proxyModel = QSortFilterProxyModel() + self.proxyModel.setFilterKeyColumn(-1) + self.proxyModel.setFilterCaseSensitivity( + Qt.CaseSensitivity.CaseInsensitive) + + def filterList(text): + self.proxyModel.setFilterFixedString(text) + + searchBar = QLineEdit() + searchBar.setPlaceholderText("Filter...") + layout.addWidget(searchBar) + searchBar.textEdited.connect(filterList) + + options = QHBoxLayout() + + layout.layout().addLayout(options) + + self.startggSetSelectionItemList = QTableView() + self.startggSetSelectionItemList.doubleClicked.connect( + lambda x: self.LoadSelectedSet()) + self.startggSetSelectionItemList.installEventFilter(self) + layout.addWidget(self.startggSetSelectionItemList) + self.startggSetSelectionItemList.setSortingEnabled(True) + self.startggSetSelectionItemList.setSelectionBehavior( + QAbstractItemView.SelectRows) + self.startggSetSelectionItemList.setEditTriggers( + QAbstractItemView.NoEditTriggers) + self.startggSetSelectionItemList.setModel(self.proxyModel) + self.startggSetSelectionItemList.horizontalHeader( + ).setSectionResizeMode(QHeaderView.Stretch) + self.startggSetSelectionItemList.resizeColumnsToContents() + + btOk = QPushButton("OK") + layout.addWidget(btOk) + btOk.clicked.connect( + lambda x: self.LoadSelectedSet() + ) + + self.resize(1200, 500) + + qr = self.frameGeometry() + cp = QApplication.primaryScreen().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft()) + + TSHTournamentDataProvider.instance.signals.get_stations_finished.connect( + self.SetStations) + + def eventFilter(self, obj, event): + if obj is self.startggSetSelectionItemList and event.type() == QEvent.KeyPress: + if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter): + self.LoadSelectedSet() + return super().eventFilter(obj, event) + + def LoadStations(self): + self.proxyModel.setSourceModel(QStandardItemModel()) + TSHTournamentDataProvider.instance.LoadStations() + + def SetStations(self, stations): + logger.info("Got stations" + str(len(stations))) + model = QStandardItemModel() + horizontal_labels = ["Type", "Id", "Stream", "Identifier"] + horizontal_labels[0] = QApplication.translate("app", "Type") + horizontal_labels[1] = QApplication.translate("app", "Id") + horizontal_labels[2] = QApplication.translate("app", "Stream") + horizontal_labels[3] = QApplication.translate("app", "Identifier") + model.setHorizontalHeaderLabels(horizontal_labels) + + if stations is not None: + for s in stations: + model.appendRow([ + QStandardItem(str(s.get("type", ""))), + QStandardItem(str(s.get("id", ""))), + QStandardItem(str(s.get("stream", ""))), + QStandardItem(str(s.get("identifier", ""))) + ]) + + model.setData( + model.index(model.rowCount()-1, 0), + s, Qt.ItemDataRole.UserRole + ) + + self.proxyModel.setSourceModel(model) + self.startggSetSelectionItemList.resizeColumnsToContents() + self.startggSetSelectionItemList.horizontalHeader( + ).setSectionResizeMode(QHeaderView.Stretch) + QApplication.processEvents() + self.resize(self.width(), self.height()) + + def LoadSelectedSet(self): + row = 0 + + if len(self.startggSetSelectionItemList.selectionModel().selectedRows()) > 0: + row = self.startggSetSelectionItemList.selectionModel().selectedRows()[ + 0].row() + + station = self.startggSetSelectionItemList.model().index( + row, 0).data(Qt.ItemDataRole.UserRole) + + self.close() + + if station: + self.parent().signals.StationSelected.emit(station) diff --git a/src/TSHTournamentDataProvider.py b/src/TSHTournamentDataProvider.py index c5fe83e4f..770cfd932 100644 --- a/src/TSHTournamentDataProvider.py +++ b/src/TSHTournamentDataProvider.py @@ -24,11 +24,13 @@ class TSHTournamentDataProviderSignals(QObject): twitch_username_updated = Signal() user_updated = Signal() get_sets_finished = Signal(list) + get_stations_finished = Signal(list) tournament_phases_updated = Signal(list) tournament_phasegroup_updated = Signal(dict) game_changed = Signal(int) stream_queue_loaded = Signal(dict) + class TSHTournamentDataProvider: instance: "TSHTournamentDataProvider" = None @@ -212,15 +214,32 @@ def LoadSets(self, showFinished): ]) self.threadPool.start(worker) - def LoadStreamSet(self, mainWindow, streamName): - streamSet = TSHTournamentDataProvider.instance.provider.GetStreamMatchId( - streamName) + def LoadStations(self): + worker = Worker(self.provider.GetStations) + worker.signals.result.connect(lambda data: [ + logger.info(data), + self.signals.get_stations_finished.emit(data) + ]) + self.threadPool.start(worker) + + def LoadStationSet(self, mainWindow): + if mainWindow.lastStationSelected: + stationSet = None - if not streamSet: - return + if mainWindow.lastStationSelected.get("type") == "stream": + stationSet = TSHTournamentDataProvider.instance.provider.GetStreamMatchId( + mainWindow.lastStationSelected.get("identifier")) + else: + stationSet = TSHTournamentDataProvider.instance.provider.GetStationMatchId( + mainWindow.lastStationSelected.get("id")) + + if not stationSet: + stationSet = {} - streamSet["auto_update"] = "stream" - mainWindow.signals.NewSetSelected.emit(streamSet) + stationSet["auto_update"] = mainWindow.lastStationSelected.get( + "type") + + mainWindow.signals.NewSetSelected.emit(stationSet) def LoadUserSet(self, mainWindow, user): _set = TSHTournamentDataProvider.instance.provider.GetUserMatchId(user) @@ -286,7 +305,7 @@ def UiMounted(self): SettingsManager.Get("TOURNAMENT_URL"), initialLoading=True) TSHTournamentDataProvider.instance.signals.twitch_username_updated.emit() TSHTournamentDataProvider.instance.signals.user_updated.emit() - + def GetProvider(self): return self.provider diff --git a/src/TournamentDataProvider/ChallongeDataProvider.py b/src/TournamentDataProvider/ChallongeDataProvider.py index 233f1eea6..5c6565d63 100644 --- a/src/TournamentDataProvider/ChallongeDataProvider.py +++ b/src/TournamentDataProvider/ChallongeDataProvider.py @@ -50,7 +50,8 @@ def __init__(self, url, threadpool, parent) -> None: i, initialized = 0, False while not initialized and i < max_iter: if i > 0: - logger.info(f"Retrying Cloudfare initialization (Attempt #{i+1})") + logger.info( + f"Retrying Cloudfare initialization (Attempt #{i+1})") try: self.scraper = cloudscraper.create_scraper(browser={ 'browser': 'firefox', @@ -63,7 +64,7 @@ def __init__(self, url, threadpool, parent) -> None: i += 1 if i >= max_iter: raise e - #TODO: Find a way to open a warning box and unload tournament if failed + # TODO: Find a way to open a warning box and unload tournament if failed def GetSlug(self): # URL with language @@ -185,6 +186,61 @@ def GetMatch(self, setId, progress_callback): return finalData + def GetStations(self, progress_callback=None): + try: + logger.info("Get stations") + + final_data = [] + + logger.info("Fetching stations") + + data = self.scraper.get( + self.GetEnglishUrl()+"/stations.json", + headers=HEADERS, + allow_redirects=True + ) + logger.info(self.GetEnglishUrl()+"/stations.json") + logger.info(str(data.text)) + + data = orjson.loads(data.text) + + for station in data: + final_data.append({ + "id": station.get("id"), + "type": "station", + "identifier": station.get("name"), + "stream": station.get("stream_url") + }) + + return final_data + + except Exception as e: + logger.error(traceback.format_exc()) + return (final_data) + return ([]) + + def GetStationMatchId(self, stationId): + stationSet = None + + try: + data = self.scraper.get( + self.GetEnglishUrl()+"/stations.json", + headers=HEADERS, + allow_redirects=True + ) + + logger.info(self.GetEnglishUrl()+"/stations.json") + logger.info(str(data.text)) + data = orjson.loads(data.text) + + for station in data: + if station.get("id") == stationId: + stationSet = deep_get(station, "match") + + except Exception as e: + logger.error(traceback.format_exc()) + return stationSet + def GetMatches(self, getFinished=False, progress_callback=None): final_data = [] @@ -638,7 +694,8 @@ def ParseMatchData(self, match): self.ParseEntrant(deep_get(match, "player1")).get("players"), self.ParseEntrant(deep_get(match, "player2")).get("players"), ], - "stream": stream, + "stream": deep_get(match, "station.stream_url", None), + "station": deep_get(match, "station.name", None), "is_current_stream_game": True if deep_get(match, "station.stream_url", None) else False, "team1score": scores[0], "team2score": scores[1], diff --git a/src/TournamentDataProvider/StartGGDataProvider.py b/src/TournamentDataProvider/StartGGDataProvider.py index f0e66e764..da127658d 100644 --- a/src/TournamentDataProvider/StartGGDataProvider.py +++ b/src/TournamentDataProvider/StartGGDataProvider.py @@ -36,6 +36,8 @@ class StartGGDataProvider(TournamentDataProvider): StreamQueueQuery = None MainPhaseQuery = None SeedsQuery = None + StationsQuery = None + StationSetsQuery = None player_seeds = {} @@ -449,6 +451,52 @@ def GetMatches(self, getFinished=False, progress_callback=None): return (final_data) return ([]) + def GetStations(self, progress_callback=None): + try: + logger.info("Get stations") + + final_data = [] + + logger.info("Fetching stations") + + data = self.QueryRequests( + "https://www.start.gg/api/-/gql", + type=requests.post, + jsonParams={ + "operationName": "Stations", + "variables": { + "eventSlug": self.url.split("start.gg/")[1], + }, + "query": StartGGDataProvider.StationsQuery + } + ) + + stations = deep_get(data, "data.event.stations.nodes", []) + queues = deep_get(data, "data.event.tournament.streamQueue", []) + + for station in stations: + final_data.append({ + "id": station.get("id"), + "identifier": station.get("number"), + "type": "station", + "stream": next((deep_get(s, "stream.streamName", None) for s in queues if str(deep_get(s, "stream.id", None)) == str(station.get("streamId"))), "") + }) + + for queue in queues: + if queue.get("stream") is not None: + stream = queue.get("stream") + final_data.append({ + "id": stream.get("id"), + "identifier": stream.get("streamName"), + "type": "stream" + }) + + return (final_data) + except Exception as e: + logger.error(traceback.format_exc()) + return (final_data) + return ([]) + def TranslateRoundName(name: str): if name == None: return "" @@ -503,6 +551,7 @@ def ParseMatchDataNewApi(self, _set): "p1_name": p1.get("entrant", {}).get("name", "") if p1 and p1.get("entrant", {}) != None else "", "p2_name": p2.get("entrant", {}).get("name", "") if p2 and p2.get("entrant", {}) != None else "", "stream": _set.get("stream", {}).get("streamName", "") if _set.get("stream", {}) != None else "", + "station": _set.get("station", {}).get("number", "") if _set.get("station", {}) != None else "", "isOnline": deep_get(_set, "event.isOnline"), } @@ -1037,6 +1086,43 @@ def GetStreamMatchId(self, streamName): return streamSet + def GetStationMatchId(self, stationId): + stationSet = None + + try: + data = self.QueryRequests( + "https://www.start.gg/api/-/gql", + type=requests.post, + jsonParams={ + "operationName": "StationSetsQuery", + "variables": { + "eventSlug": self.url.split("start.gg/")[1], + "filters": { + "state": [1, 2, 4, 5, 6], + "hideEmpty": True + } + }, + "query": StartGGDataProvider.StationSetsQuery + } + ) + + sets = deep_get(data, "data.event.sets.nodes", []) + + print("SETS", sets, stationId) + + sets = [s for s in sets if str(deep_get( + s, "station.id", "-1")) == str(stationId)] + + print("SETS", sets) + + if len(sets) > 0: + stationSet = sets[0] + + except Exception as e: + logger.error(traceback.format_exc()) + + return stationSet + def GetUserMatchId(self, user): matches = re.match( r".*start.gg/(user/[^/]*)", user) @@ -1672,3 +1758,9 @@ def GetStandings(self, playerNumber, progress_callback): f = open("src/TournamentDataProvider/StartGGTournamentSeedsQuery.txt", 'r') StartGGDataProvider.SeedsQuery = f.read() + +f = open("src/TournamentDataProvider/StartGGStationsQuery.txt", 'r') +StartGGDataProvider.StationsQuery = f.read() + +f = open("src/TournamentDataProvider/StartGGStationSetsQuery.txt", 'r') +StartGGDataProvider.StationSetsQuery = f.read() diff --git a/src/TournamentDataProvider/StartGGSetsQuery.txt b/src/TournamentDataProvider/StartGGSetsQuery.txt index ee212ecb6..4673bf4a3 100644 --- a/src/TournamentDataProvider/StartGGSetsQuery.txt +++ b/src/TournamentDataProvider/StartGGSetsQuery.txt @@ -34,8 +34,11 @@ query EventMatchListQuery($eventSlug: String!, $page: Int = 1, $filters: SetFilt } } stream { - streamName - streamSource + streamName + streamSource + } + station { + number } } } diff --git a/src/TournamentDataProvider/StartGGStationSetsQuery.txt b/src/TournamentDataProvider/StartGGStationSetsQuery.txt new file mode 100644 index 000000000..c02d7f8cc --- /dev/null +++ b/src/TournamentDataProvider/StartGGStationSetsQuery.txt @@ -0,0 +1,13 @@ +query StationSetsQuery($eventSlug: String!, $filters: SetFilters = {}) { + event(slug: $eventSlug) { + sets(page: 1, perPage: 999, filters: $filters) { + nodes { + id + state + station { + id + } + } + } + } +} \ No newline at end of file diff --git a/src/TournamentDataProvider/StartGGStationsQuery.txt b/src/TournamentDataProvider/StartGGStationsQuery.txt new file mode 100644 index 000000000..3089cafa8 --- /dev/null +++ b/src/TournamentDataProvider/StartGGStationsQuery.txt @@ -0,0 +1,28 @@ +query Stations($eventSlug: String!) { + event(slug: $eventSlug) { + stations(page: 1, perPage: 999) { + nodes { + id + clusterNumber + clusterPrefix + enabled + identifier + number + prefix + queueDepth + state + streamId + } + } + tournament { + streamQueue { + id + stream { + id + streamName + streamSource + } + } + } + } +} \ No newline at end of file diff --git a/src/TournamentDataProvider/TournamentDataProvider.py b/src/TournamentDataProvider/TournamentDataProvider.py index 81a46c240..283e4f2ac 100644 --- a/src/TournamentDataProvider/TournamentDataProvider.py +++ b/src/TournamentDataProvider/TournamentDataProvider.py @@ -23,6 +23,9 @@ def GetMatch(self, setId, progress_callback=None): def GetMatches(self, getFinished=False, progress_callback=None): pass + def GetStations(self, progress_callback=None): + pass + def GetStreamQueue(self, streamName, progress_callback=None): pass diff --git a/src/i18n/TSH_de.ts b/src/i18n/TSH_de.ts index 1e3d99462..57bf8f860 100644 --- a/src/i18n/TSH_de.ts +++ b/src/i18n/TSH_de.ts @@ -755,7 +755,7 @@ p, li { white-space: pre-wrap; } - + Characters per player Charaktere pro Slot @@ -1188,47 +1188,47 @@ p, li { white-space: pre-wrap; } {0} wird heruntergeladen... - + Paste the tournament URL. URL des Turniers einfügen. - + For StartGG, the link must contain the /event/ part Wird StartGG verwendet, so muss der Link den Teil mit /event/ beinhalten - + Set tournament URL Turnier-URL einstellen - + Set Twitch username Twitch-Usernamen einstellen - + Twitch Username: Twitch-Username: - + Paste the URL to the player's StartGG profile URL des Spielerprofils auf StartGG einfügen - + Insert the player's name in bracket Spielername einfügen - + Invalid tournament data provider Ungültiger Turnierdaten-Provider - + Set player Spieler auswählen @@ -1243,24 +1243,30 @@ p, li { white-space: pre-wrap; } - + + Stream - + + Station + + + + Phase Turnierphase - + Match - + Player {0} Spieler {0} @@ -1301,61 +1307,63 @@ p, li { white-space: pre-wrap; } - + Players per team Spieler pro Team - + Generate Thumbnail Thumbnail erstellen - + Real Name Klarname - + Twitter Twitter - + Location Staat/Region - + Characters Charaktere - + Pronouns Pronomen - - + + Load set Set laden - - - Load current stream set aktuelles Stream-Set laden - - + + Track station sets + + + + + TEAM {0} TEAM {0} @@ -1365,22 +1373,22 @@ p, li { white-space: pre-wrap; } - + Warning ACHTUNG - + Load set from {0} Set von {0} laden - + Load user set ({0}) User-Set {0} laden - + Load user set User-Set laden @@ -1465,20 +1473,38 @@ p, li { white-space: pre-wrap; } Custom Flags + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation - - + ( - - + ) @@ -1582,12 +1608,12 @@ p, li { white-space: pre-wrap; } thumb_app - + TSH - Thumbnail - + The thumbnail has been generated here: Thumbnail wurde erstellt: diff --git a/src/i18n/TSH_en.ts b/src/i18n/TSH_en.ts index 1308e0942..759087942 100644 --- a/src/i18n/TSH_en.ts +++ b/src/i18n/TSH_en.ts @@ -750,7 +750,7 @@ p, li { white-space: pre-wrap; } - + Characters per player @@ -801,7 +801,7 @@ p, li { white-space: pre-wrap; } - + Warning @@ -1112,8 +1112,8 @@ p, li { white-space: pre-wrap; } - + Player {0} @@ -1124,76 +1124,74 @@ p, li { white-space: pre-wrap; } - + Players per team - + Generate Thumbnail - + Real Name - + Twitter - + Location - + Characters - + Pronouns - - + + Load set - - - - Load current stream set + + Track station sets - - + + TEAM {0} - + Load set from {0} - + Load user set ({0}) - + Load user set @@ -1319,17 +1317,23 @@ p, li { white-space: pre-wrap; } - + + Stream - + + Station + + + + Phase - + Match @@ -1371,47 +1375,47 @@ p, li { white-space: pre-wrap; } - + Paste the tournament URL. - + For StartGG, the link must contain the /event/ part - + Set tournament URL - + Set Twitch username - + Twitch Username: - + Paste the URL to the player's StartGG profile - + Insert the player's name in bracket - + Invalid tournament data provider - + Set player @@ -1420,20 +1424,38 @@ p, li { white-space: pre-wrap; } Custom Flags + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation - - + ( - - + ) @@ -1569,12 +1591,12 @@ p, li { white-space: pre-wrap; } thumb_app - + TSH - Thumbnail - + The thumbnail has been generated here: diff --git a/src/i18n/TSH_es.ts b/src/i18n/TSH_es.ts index d551401ee..d41b8d6a4 100644 --- a/src/i18n/TSH_es.ts +++ b/src/i18n/TSH_es.ts @@ -760,7 +760,7 @@ p, li { white-space: pre-wrap; } - + Characters per player Personajes por jugador @@ -1197,47 +1197,47 @@ p, li { white-space: pre-wrap; } Descargando {0}... - + Paste the tournament URL. Pega la URL del torneo. - + For StartGG, the link must contain the /event/ part Para pegar desde StartGG, el link debe contener la parte /evento/ - + Set tournament URL Establecer URL del torneo - + Set Twitch username Establecer usuario de Twitch - + Twitch Username: Usuario de Twitch: - + Paste the URL to the player's StartGG profile Pega la URL del perfil de StartGG del jugador - + Insert the player's name in bracket Inserta el nombre del jugador en el bracket - + Invalid tournament data provider Proveedor de datos de torneo no válido - + Set player Establecer jugador @@ -1252,17 +1252,23 @@ p, li { white-space: pre-wrap; } - + + Stream - + + Station + + + + Phase Fase - + Match Ronda @@ -1289,8 +1295,8 @@ p, li { white-space: pre-wrap; } - + Player {0} Jugador {0} @@ -1310,61 +1316,63 @@ p, li { white-space: pre-wrap; } - + Players per team Jugadores por equipo - + Generate Thumbnail Generar Miniatura - + Real Name Nombre Real - + Twitter - + Location Localidad - + Characters Personajes - + Pronouns Pronombres - - + + Load set Cargar set - - - Load current stream set Cargar set actual desde el stream - - + + Track station sets + + + + + TEAM {0} EQUIPO {0} @@ -1374,22 +1382,22 @@ p, li { white-space: pre-wrap; } - + Warning Aviso - + Load set from {0} Cargar set de {0} - + Load user set ({0}) Cargar set de usuario ({0}) - + Load user set Cargar set de usuario @@ -1478,6 +1486,26 @@ p, li { white-space: pre-wrap; } Custom Flags + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation @@ -1493,15 +1521,13 @@ p, li { white-space: pre-wrap; } - - + ( - - + ) @@ -1605,12 +1631,12 @@ p, li { white-space: pre-wrap; } - + TSH - Thumbnail - + The thumbnail has been generated here: La miniatura se generó aquí: diff --git a/src/i18n/TSH_fr.ts b/src/i18n/TSH_fr.ts index 8776bd3e8..31f3b8090 100644 --- a/src/i18n/TSH_fr.ts +++ b/src/i18n/TSH_fr.ts @@ -1189,7 +1189,7 @@ p, li { white-space: pre-wrap; } - + Characters per player Nombre de personnages par joueur @@ -1216,8 +1216,8 @@ p, li { white-space: pre-wrap; } - + Player {0} Joueur {0} @@ -1267,7 +1267,7 @@ p, li { white-space: pre-wrap; } Stage - + Players per team Nombre de joueurs par équipe @@ -1276,26 +1276,28 @@ p, li { white-space: pre-wrap; } Générer la miniature - + Generate Thumbnail Générer la miniature - - + + Load set Charger un set - - - Load current stream set Charger le set actuel du stream - - + + Track station sets + + + + + TEAM {0} ÉQUIPE {0} @@ -1305,52 +1307,52 @@ p, li { white-space: pre-wrap; } - + Warning Attention - + Load set from {0} Charger un set depuis {0} - + Load user set ({0}) Charger le set de l'utilisateur {0} - + Load user set Charger un set utilisateur - + Paste the tournament URL. Entrez l'URL du tournoi. - + For StartGG, the link must contain the /event/ part Pour StartGG, le lien doit contenir la partie /event/ - + Set tournament URL Définir l'URL du tournoi - + Set Twitch username Définir le nom d'utilisateur Twitch - + Twitch Username: Nom d'utilisateur Twitch : - + Paste the URL to the player's StartGG profile Entrez l'URL du profil joueur StartGG @@ -1365,17 +1367,23 @@ p, li { white-space: pre-wrap; } Afficher uniquement les paires complètes - + + Stream Stream - + + Station + + + + Phase Phase - + Match Match @@ -1389,17 +1397,17 @@ p, li { white-space: pre-wrap; } Nom d'utilisateur Twitch : - + Insert the player's name in bracket Entrez le nom du joueur tel qu'affiché dans l'arbre de tournoi - + Invalid tournament data provider Fournisseur de données de tournoi invalide - + Set player Définir le joueur @@ -1424,31 +1432,31 @@ p, li { white-space: pre-wrap; } - + Real Name Nom Réel - + Twitter Twitter - + Location Lieu - + Characters Personnages - + Pronouns Pronoms @@ -1521,6 +1529,26 @@ p, li { white-space: pre-wrap; } Custom Flags Drapeaux additionnels + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation @@ -1536,15 +1564,13 @@ p, li { white-space: pre-wrap; } - - + ( ( - - + ) ) @@ -1652,12 +1678,12 @@ p, li { white-space: pre-wrap; } - + TSH - Thumbnail TSH - Miniature - + The thumbnail has been generated here: La miniature a été enregistrée à l'emplacement suivant : diff --git a/src/i18n/TSH_it.ts b/src/i18n/TSH_it.ts index 97e6beea5..a544eea01 100644 --- a/src/i18n/TSH_it.ts +++ b/src/i18n/TSH_it.ts @@ -734,7 +734,7 @@ p, li { white-space: pre-wrap; } - + Warning Avvertimento @@ -1136,7 +1136,7 @@ p, li { white-space: pre-wrap; } - + Characters per player Personaggi per giocatore @@ -1168,8 +1168,8 @@ p, li { white-space: pre-wrap; } - + Player {0} Giocatore {0} @@ -1210,76 +1210,74 @@ p, li { white-space: pre-wrap; } - + Players per team - + Generate Thumbnail Generare la miniatura - + Real Name Nome legale - + Twitter - + Location - + Characters Personaggi - + Pronouns - - + + Load set - - - - Load current stream set + + Track station sets - - + + TEAM {0} - + Load set from {0} - + Load user set ({0}) - + Load user set @@ -1299,17 +1297,23 @@ p, li { white-space: pre-wrap; } - + + Stream - + + Station + + + + Phase - + Match @@ -1371,47 +1375,47 @@ p, li { white-space: pre-wrap; } - + Paste the tournament URL. - + For StartGG, the link must contain the /event/ part - + Set tournament URL - + Set Twitch username - + Twitch Username: - + Paste the URL to the player's StartGG profile - + Insert the player's name in bracket - + Invalid tournament data provider - + Set player @@ -1420,20 +1424,38 @@ p, li { white-space: pre-wrap; } Custom Flags + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation - - + ( - - + ) @@ -1537,12 +1559,12 @@ p, li { white-space: pre-wrap; } thumb_app - + TSH - Thumbnail TSH - Miniatura - + The thumbnail has been generated here: diff --git a/src/i18n/TSH_ja.ts b/src/i18n/TSH_ja.ts index fc722f74e..97ae711f0 100644 --- a/src/i18n/TSH_ja.ts +++ b/src/i18n/TSH_ja.ts @@ -1161,7 +1161,7 @@ p, li { white-space: pre-wrap; } - + Characters per player 各プレイヤーの使用キャラクター数 @@ -1188,8 +1188,8 @@ p, li { white-space: pre-wrap; } - + Player {0} プレイヤー{0} @@ -1230,47 +1230,47 @@ p, li { white-space: pre-wrap; } banByMaxGamesのテキストが無効です。 - + Paste the tournament URL. 大会のURLをここに貼って下さい - + For StartGG, the link must contain the /event/ part StartGGのリンクには/event/partを含めて下さい - + Set tournament URL 大会のURLを貼る - + Set Twitch username Twitchでのユーザー名を入力 - + Twitch Username: Twitchでのユーザー名: - + Paste the URL to the player's StartGG profile プレイヤーのStartGGのプロフィールへのURLをここに貼ってください - + Insert the player's name in bracket ブラケット表にプレイヤー名を記入して下さい - + Invalid tournament data provider 無効な大会データプロバイダです - + Set player プレイヤーを選ぶ @@ -1285,17 +1285,23 @@ p, li { white-space: pre-wrap; } - + + Stream 配信 - + + Station + + + + Phase フェーズ - + Match ラウンド @@ -1333,49 +1339,49 @@ p, li { white-space: pre-wrap; } ステージ - + Players per team 各チームのプレイヤー数 - + Real Name 本名 - + Twitter ツイッター - + Location 本拠地 - + Characters 使用キャラクター - + Pronouns 代名詞 - - + + Load set 対戦データをロードする - - + + TEAM {0} チーム{0} @@ -1385,34 +1391,36 @@ p, li { white-space: pre-wrap; } - + Warning 注意 - + Load set from {0} {0}から対戦データをロードする - - - Load current stream set 配信中の対戦データをロードする - + Generate Thumbnail サムネイルを作成する - + + Track station sets + + + + Load user set ({0}) ユーザーの対戦データ({0})をロードする - + Load user set ユーザーの対戦データをロードする @@ -1481,6 +1489,26 @@ p, li { white-space: pre-wrap; } Custom Flags + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation @@ -1496,15 +1524,13 @@ p, li { white-space: pre-wrap; } - - + ( ( - - + ) ) @@ -1608,12 +1634,12 @@ p, li { white-space: pre-wrap; } - + TSH - Thumbnail TSH - サムネイル - + The thumbnail has been generated here: サムネイルはここに作成されました: diff --git a/src/i18n/TSH_pt-BR.ts b/src/i18n/TSH_pt-BR.ts index 0b0acedcb..b96506a27 100644 --- a/src/i18n/TSH_pt-BR.ts +++ b/src/i18n/TSH_pt-BR.ts @@ -1208,7 +1208,7 @@ p, li { white-space: pre-wrap; } - + Characters per player Personagens por jogador @@ -1235,8 +1235,8 @@ p, li { white-space: pre-wrap; } - + Player {0} Jogador {0} @@ -1277,32 +1277,32 @@ p, li { white-space: pre-wrap; } O texto para bans por número de partidas é inválido. - + Paste the tournament URL. Cole a URL do torneio. - + For StartGG, the link must contain the /event/ part Para o StartGG, o link deve conter /evento/ - + Set tournament URL Definir a URL do torneio - + Set Twitch username Definir o nome de usuário do Twitch - + Twitch Username: Nome de usuário no Twitch: - + Paste the URL to the player's StartGG profile Cole a URL para o perfil do jogador no StartGG @@ -1317,17 +1317,23 @@ p, li { white-space: pre-wrap; } - + + Stream - + + Station + + + + Phase Fase - + Match Partida @@ -1345,17 +1351,17 @@ p, li { white-space: pre-wrap; } Usuário do Twitch: - + Insert the player's name in bracket Insira o nome do jogador na bracket - + Invalid tournament data provider Provedor de dados de torneio inválido - + Set player Definir jogador @@ -1388,7 +1394,7 @@ p, li { white-space: pre-wrap; } - + Players per team Jogadores por time @@ -1398,43 +1404,43 @@ p, li { white-space: pre-wrap; } - + Real Name Nome Real - + Twitter - + Location Local - + Characters Personagens - + Pronouns Pronomes - - + + Load set Carregar set - - + + TEAM {0} TIME {0} @@ -1444,34 +1450,36 @@ p, li { white-space: pre-wrap; } - + Warning Aviso - + Load set from {0} Carregar set do {0} - - - Load current stream set Carregar set atual do stream - + Generate Thumbnail Gerar Thumbnail - + + Track station sets + + + + Load user set ({0}) Carregar set do usuário ({0}) - + Load user set Carregar set do usuário @@ -1544,6 +1552,26 @@ p, li { white-space: pre-wrap; } Custom Flags + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation @@ -1559,15 +1587,13 @@ p, li { white-space: pre-wrap; } - - + ( ( - - + ) ) @@ -1707,12 +1733,12 @@ p, li { white-space: pre-wrap; } - + TSH - Thumbnail - + The thumbnail has been generated here: A miniatura foi gerada aqui: diff --git a/src/i18n/TSH_zh-CN.ts b/src/i18n/TSH_zh-CN.ts index b374ba406..8ace07ddb 100644 --- a/src/i18n/TSH_zh-CN.ts +++ b/src/i18n/TSH_zh-CN.ts @@ -734,7 +734,7 @@ p, li { white-space: pre-wrap; } - + Warning @@ -1136,7 +1136,7 @@ p, li { white-space: pre-wrap; } - + Characters per player @@ -1168,8 +1168,8 @@ p, li { white-space: pre-wrap; } - + Player {0} @@ -1210,76 +1210,74 @@ p, li { white-space: pre-wrap; } - + Players per team - + Generate Thumbnail - + Real Name - + Twitter - + Location - + Characters 角色 - + Pronouns - - + + Load set - - - - Load current stream set + + Track station sets - - + + TEAM {0} - + Load set from {0} - + Load user set ({0}) - + Load user set @@ -1299,17 +1297,23 @@ p, li { white-space: pre-wrap; } - + + Stream - + + Station + + + + Phase - + Match @@ -1371,47 +1375,47 @@ p, li { white-space: pre-wrap; } - + Paste the tournament URL. - + For StartGG, the link must contain the /event/ part - + Set tournament URL - + Set Twitch username - + Twitch Username: - + Paste the URL to the player's StartGG profile - + Insert the player's name in bracket - + Invalid tournament data provider - + Set player @@ -1420,20 +1424,38 @@ p, li { white-space: pre-wrap; } Custom Flags + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation - - + ( - - + ) @@ -1537,12 +1559,12 @@ p, li { white-space: pre-wrap; } thumb_app - + TSH - Thumbnail TSH - 缩略图 - + The thumbnail has been generated here: diff --git a/src/i18n/TSH_zh-TW.ts b/src/i18n/TSH_zh-TW.ts index 8264cb61a..62f540a5e 100644 --- a/src/i18n/TSH_zh-TW.ts +++ b/src/i18n/TSH_zh-TW.ts @@ -734,7 +734,7 @@ p, li { white-space: pre-wrap; } - + Warning @@ -1136,7 +1136,7 @@ p, li { white-space: pre-wrap; } - + Characters per player @@ -1168,8 +1168,8 @@ p, li { white-space: pre-wrap; } - + Player {0} @@ -1210,76 +1210,74 @@ p, li { white-space: pre-wrap; } - + Players per team - + Generate Thumbnail - + Real Name - + Twitter - + Location - + Characters 角色 - + Pronouns - - + + Load set - - - - Load current stream set + + Track station sets - - + + TEAM {0} - + Load set from {0} - + Load user set ({0}) - + Load user set @@ -1299,17 +1297,23 @@ p, li { white-space: pre-wrap; } - + + Stream - + + Station + + + + Phase - + Match @@ -1371,47 +1375,47 @@ p, li { white-space: pre-wrap; } - + Paste the tournament URL. - + For StartGG, the link must contain the /event/ part - + Set tournament URL - + Set Twitch username - + Twitch Username: - + Paste the URL to the player's StartGG profile - + Insert the player's name in bracket - + Invalid tournament data provider - + Set player @@ -1420,20 +1424,38 @@ p, li { white-space: pre-wrap; } Custom Flags + + + Select a station + + + + + Type + + + + + Id + + + + + Identifier + + punctuation - - + ( - - + ) @@ -1537,12 +1559,12 @@ p, li { white-space: pre-wrap; } thumb_app - + TSH - Thumbnail - + The thumbnail has been generated here: