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; }
-
+
Charaktere pro Slot
@@ -1188,47 +1188,47 @@ p, li { white-space: pre-wrap; }
{0} wird heruntergeladen...
-
+
URL des Turniers einfügen.
-
+
Wird StartGG verwendet, so muss der Link den Teil mit /event/ beinhalten
-
+
Turnier-URL einstellen
-
+
Twitch-Usernamen einstellen
-
+
Twitch-Username:
-
+
URL des Spielerprofils auf StartGG einfügen
-
+
Spielername einfügen
-
+
Ungültiger Turnierdaten-Provider
-
+
Spieler auswählen
@@ -1243,24 +1243,30 @@ p, li { white-space: pre-wrap; }
-
+
+
-
+
+
+
+
+
+
Turnierphase
-
+
-
+
Spieler {0}
@@ -1301,61 +1307,63 @@ p, li { white-space: pre-wrap; }
-
+
Spieler pro Team
-
+
Thumbnail erstellen
-
+
Klarname
-
+
Twitter
-
+
Staat/Region
-
+
Charaktere
-
+
Pronomen
-
-
+
+
Set laden
-
-
-
aktuelles Stream-Set laden
-
-
+
+
+
+
+
+
+
TEAM {0}
@@ -1365,22 +1373,22 @@ p, li { white-space: pre-wrap; }
-
+
ACHTUNG
-
+
Set von {0} laden
-
+
User-Set {0} laden
-
+
User-Set laden
@@ -1465,20 +1473,38 @@ p, li { white-space: pre-wrap; }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
-
-
+
-
-
+
@@ -1582,12 +1608,12 @@ p, li { white-space: pre-wrap; }
thumb_app
-
+
-
+
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; }
-
+
@@ -801,7 +801,7 @@ p, li { white-space: pre-wrap; }
-
+
@@ -1112,8 +1112,8 @@ p, li { white-space: pre-wrap; }
-
+
@@ -1124,76 +1124,74 @@ p, li { white-space: pre-wrap; }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
-
+
+
-
-
-
-
+
+
-
-
+
+
-
+
-
+
-
+
@@ -1319,17 +1317,23 @@ p, li { white-space: pre-wrap; }
-
+
+
-
+
+
+
+
+
+
-
+
@@ -1371,47 +1375,47 @@ p, li { white-space: pre-wrap; }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1420,20 +1424,38 @@ p, li { white-space: pre-wrap; }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
-
-
+
-
-
+
@@ -1569,12 +1591,12 @@ p, li { white-space: pre-wrap; }
thumb_app
-
+
-
+
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; }
-
+
Personajes por jugador
@@ -1197,47 +1197,47 @@ p, li { white-space: pre-wrap; }
Descargando {0}...
-
+
Pega la URL del torneo.
-
+
Para pegar desde StartGG, el link debe contener la parte /evento/
-
+
Establecer URL del torneo
-
+
Establecer usuario de Twitch
-
+
Usuario de Twitch:
-
+
Pega la URL del perfil de StartGG del jugador
-
+
Inserta el nombre del jugador en el bracket
-
+
Proveedor de datos de torneo no válido
-
+
Establecer jugador
@@ -1252,17 +1252,23 @@ p, li { white-space: pre-wrap; }
-
+
+
-
+
+
+
+
+
+
Fase
-
+
Ronda
@@ -1289,8 +1295,8 @@ p, li { white-space: pre-wrap; }
-
+
Jugador {0}
@@ -1310,61 +1316,63 @@ p, li { white-space: pre-wrap; }
-
+
Jugadores por equipo
-
+
Generar Miniatura
-
+
Nombre Real
-
+
-
+
Localidad
-
+
Personajes
-
+
Pronombres
-
-
+
+
Cargar set
-
-
-
Cargar set actual desde el stream
-
-
+
+
+
+
+
+
+
EQUIPO {0}
@@ -1374,22 +1382,22 @@ p, li { white-space: pre-wrap; }
-
+
Aviso
-
+
Cargar set de {0}
-
+
Cargar set de usuario ({0})
-
+
Cargar set de usuario
@@ -1478,6 +1486,26 @@ p, li { white-space: pre-wrap; }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
@@ -1493,15 +1521,13 @@ p, li { white-space: pre-wrap; }
-
-
+
-
-
+
@@ -1605,12 +1631,12 @@ p, li { white-space: pre-wrap; }
-
+
-
+
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; }
-
+
Nombre de personnages par joueur
@@ -1216,8 +1216,8 @@ p, li { white-space: pre-wrap; }
-
+
Joueur {0}
@@ -1267,7 +1267,7 @@ p, li { white-space: pre-wrap; }
Stage
-
+
Nombre de joueurs par équipe
@@ -1276,26 +1276,28 @@ p, li { white-space: pre-wrap; }
Générer la miniature
-
+
Générer la miniature
-
-
+
+
Charger un set
-
-
-
Charger le set actuel du stream
-
-
+
+
+
+
+
+
+
ÉQUIPE {0}
@@ -1305,52 +1307,52 @@ p, li { white-space: pre-wrap; }
-
+
Attention
-
+
Charger un set depuis {0}
-
+
Charger le set de l'utilisateur {0}
-
+
Charger un set utilisateur
-
+
Entrez l'URL du tournoi.
-
+
Pour StartGG, le lien doit contenir la partie /event/
-
+
Définir l'URL du tournoi
-
+
Définir le nom d'utilisateur Twitch
-
+
Nom d'utilisateur Twitch :
-
+
Entrez l'URL du profil joueur StartGG
@@ -1365,17 +1367,23 @@ p, li { white-space: pre-wrap; }
Afficher uniquement les paires complètes
-
+
+
Stream
-
+
+
+
+
+
+
Phase
-
+
Match
@@ -1389,17 +1397,17 @@ p, li { white-space: pre-wrap; }
Nom d'utilisateur Twitch :
-
+
Entrez le nom du joueur tel qu'affiché dans l'arbre de tournoi
-
+
Fournisseur de données de tournoi invalide
-
+
Définir le joueur
@@ -1424,31 +1432,31 @@ p, li { white-space: pre-wrap; }
-
+
Nom Réel
-
+
Twitter
-
+
Lieu
-
+
Personnages
-
+
Pronoms
@@ -1521,6 +1529,26 @@ p, li { white-space: pre-wrap; }
Drapeaux additionnels
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
@@ -1536,15 +1564,13 @@ p, li { white-space: pre-wrap; }
-
-
+
(
-
-
+
)
@@ -1652,12 +1678,12 @@ p, li { white-space: pre-wrap; }
-
+
TSH - Miniature
-
+
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; }
-
+
Avvertimento
@@ -1136,7 +1136,7 @@ p, li { white-space: pre-wrap; }
-
+
Personaggi per giocatore
@@ -1168,8 +1168,8 @@ p, li { white-space: pre-wrap; }
-
+
Giocatore {0}
@@ -1210,76 +1210,74 @@ p, li { white-space: pre-wrap; }
-
+
-
+
Generare la miniatura
-
+
Nome legale
-
+
-
+
-
+
Personaggi
-
+
-
-
+
+
-
-
-
-
+
+
-
-
+
+
-
+
-
+
-
+
@@ -1299,17 +1297,23 @@ p, li { white-space: pre-wrap; }
-
+
+
-
+
+
+
+
+
+
-
+
@@ -1371,47 +1375,47 @@ p, li { white-space: pre-wrap; }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1420,20 +1424,38 @@ p, li { white-space: pre-wrap; }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
-
-
+
-
-
+
@@ -1537,12 +1559,12 @@ p, li { white-space: pre-wrap; }
thumb_app
-
+
TSH - Miniatura
-
+
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; }
-
+
各プレイヤーの使用キャラクター数
@@ -1188,8 +1188,8 @@ p, li { white-space: pre-wrap; }
-
+
プレイヤー{0}
@@ -1230,47 +1230,47 @@ p, li { white-space: pre-wrap; }
banByMaxGamesのテキストが無効です。
-
+
大会のURLをここに貼って下さい
-
+
StartGGのリンクには/event/partを含めて下さい
-
+
大会のURLを貼る
-
+
Twitchでのユーザー名を入力
-
+
Twitchでのユーザー名:
-
+
プレイヤーのStartGGのプロフィールへのURLをここに貼ってください
-
+
ブラケット表にプレイヤー名を記入して下さい
-
+
無効な大会データプロバイダです
-
+
プレイヤーを選ぶ
@@ -1285,17 +1285,23 @@ p, li { white-space: pre-wrap; }
-
+
+
配信
-
+
+
+
+
+
+
フェーズ
-
+
ラウンド
@@ -1333,49 +1339,49 @@ p, li { white-space: pre-wrap; }
ステージ
-
+
各チームのプレイヤー数
-
+
本名
-
+
ツイッター
-
+
本拠地
-
+
使用キャラクター
-
+
代名詞
-
-
+
+
対戦データをロードする
-
-
+
+
チーム{0}
@@ -1385,34 +1391,36 @@ p, li { white-space: pre-wrap; }
-
+
注意
-
+
{0}から対戦データをロードする
-
-
-
配信中の対戦データをロードする
-
+
サムネイルを作成する
-
+
+
+
+
+
+
ユーザーの対戦データ({0})をロードする
-
+
ユーザーの対戦データをロードする
@@ -1481,6 +1489,26 @@ p, li { white-space: pre-wrap; }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
@@ -1496,15 +1524,13 @@ p, li { white-space: pre-wrap; }
-
-
+
(
-
-
+
)
@@ -1608,12 +1634,12 @@ p, li { white-space: pre-wrap; }
-
+
TSH - サムネイル
-
+
サムネイルはここに作成されました:
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; }
-
+
Personagens por jogador
@@ -1235,8 +1235,8 @@ p, li { white-space: pre-wrap; }
-
+
Jogador {0}
@@ -1277,32 +1277,32 @@ p, li { white-space: pre-wrap; }
O texto para bans por número de partidas é inválido.
-
+
Cole a URL do torneio.
-
+
Para o StartGG, o link deve conter /evento/
-
+
Definir a URL do torneio
-
+
Definir o nome de usuário do Twitch
-
+
Nome de usuário no Twitch:
-
+
Cole a URL para o perfil do jogador no StartGG
@@ -1317,17 +1317,23 @@ p, li { white-space: pre-wrap; }
-
+
+
-
+
+
+
+
+
+
Fase
-
+
Partida
@@ -1345,17 +1351,17 @@ p, li { white-space: pre-wrap; }
Usuário do Twitch:
-
+
Insira o nome do jogador na bracket
-
+
Provedor de dados de torneio inválido
-
+
Definir jogador
@@ -1388,7 +1394,7 @@ p, li { white-space: pre-wrap; }
-
+
Jogadores por time
@@ -1398,43 +1404,43 @@ p, li { white-space: pre-wrap; }
-
+
Nome Real
-
+
-
+
Local
-
+
Personagens
-
+
Pronomes
-
-
+
+
Carregar set
-
-
+
+
TIME {0}
@@ -1444,34 +1450,36 @@ p, li { white-space: pre-wrap; }
-
+
Aviso
-
+
Carregar set do {0}
-
-
-
Carregar set atual do stream
-
+
Gerar Thumbnail
-
+
+
+
+
+
+
Carregar set do usuário ({0})
-
+
Carregar set do usuário
@@ -1544,6 +1552,26 @@ p, li { white-space: pre-wrap; }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
@@ -1559,15 +1587,13 @@ p, li { white-space: pre-wrap; }
-
-
+
(
-
-
+
)
@@ -1707,12 +1733,12 @@ p, li { white-space: pre-wrap; }
-
+
-
+
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; }
-
+
@@ -1136,7 +1136,7 @@ p, li { white-space: pre-wrap; }
-
+
@@ -1168,8 +1168,8 @@ p, li { white-space: pre-wrap; }
-
+
@@ -1210,76 +1210,74 @@ p, li { white-space: pre-wrap; }
-
+
-
+
-
+
-
+
-
+
-
+
角色
-
+
-
-
+
+
-
-
-
-
+
+
-
-
+
+
-
+
-
+
-
+
@@ -1299,17 +1297,23 @@ p, li { white-space: pre-wrap; }
-
+
+
-
+
+
+
+
+
+
-
+
@@ -1371,47 +1375,47 @@ p, li { white-space: pre-wrap; }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1420,20 +1424,38 @@ p, li { white-space: pre-wrap; }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
-
-
+
(
-
-
+
)
@@ -1537,12 +1559,12 @@ p, li { white-space: pre-wrap; }
thumb_app
-
+
TSH - 缩略图
-
+
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; }
-
+
@@ -1136,7 +1136,7 @@ p, li { white-space: pre-wrap; }
-
+
@@ -1168,8 +1168,8 @@ p, li { white-space: pre-wrap; }
-
+
@@ -1210,76 +1210,74 @@ p, li { white-space: pre-wrap; }
-
+
-
+
-
+
-
+
-
+
-
+
角色
-
+
-
-
+
+
-
-
-
-
+
+
-
-
+
+
-
+
-
+
-
+
@@ -1299,17 +1297,23 @@ p, li { white-space: pre-wrap; }
-
+
+
-
+
+
+
+
+
+
-
+
@@ -1371,47 +1375,47 @@ p, li { white-space: pre-wrap; }
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -1420,20 +1424,38 @@ p, li { white-space: pre-wrap; }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
punctuation
-
-
+
(
-
-
+
)
@@ -1537,12 +1559,12 @@ p, li { white-space: pre-wrap; }
thumb_app
-
+
-
+