aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore160
-rw-r--r--currencies.py26
-rw-r--r--yt-livechat-stats.py108
3 files changed, 294 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..68bc17f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,160 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
diff --git a/currencies.py b/currencies.py
new file mode 100644
index 0000000..d95df95
--- /dev/null
+++ b/currencies.py
@@ -0,0 +1,26 @@
+import requests
+import json
+
+class CurrencyConv:
+ def __init__(self, currency_json_url: str="https://open.er-api.com/v6/latest/USD"):
+ try:
+ self._currency_data = json.loads(requests.get(currency_json_url).text)
+ except:
+ raise Exception("Unable to load currency data from " + currency_json_url)
+ self._currency_rates = self._currency_data["rates"]
+
+ def convert(self, amount: float, from_currency: str, to_currency: str) -> float:
+ # handle some conversion errors
+ from_currency = from_currency.replace("\xa0", "").strip().upper()
+ from_currency= from_currency.replace("₱", "PHP")
+ from_currency = from_currency.replace("¥", "JPY")
+ try:
+ if from_currency == to_currency:
+ return amount
+ return amount * self._currency_rates[to_currency] / self._currency_rates[from_currency]
+ except:
+ raise Exception("Unable to convert from " + from_currency + " to " + to_currency)
+
+if __name__ == "__main__":
+ converter = CurrencyConv()
+ print(converter.convert(100.0, "SGD", "USD"))
diff --git a/yt-livechat-stats.py b/yt-livechat-stats.py
new file mode 100644
index 0000000..1639121
--- /dev/null
+++ b/yt-livechat-stats.py
@@ -0,0 +1,108 @@
+import pytchat
+import time
+import argparse
+from currencies import CurrencyConv
+import curses
+import os
+
+
+class StreamEndedError(Exception):
+ pass
+
+def main(stdscr, video_id: str, args: argparse.Namespace):
+ curses.curs_set(0)
+ height, width = stdscr.getmaxyx()
+ max_num_messages = -(height-4) if args.max_messages is None else args.max_messages
+ stdscr.nodelay(True)
+ chat_window = curses.newwin(height - 4, width, 8, 0)
+
+ chat = pytchat.create(video_id=video_id)
+
+ message_count = 0
+ current_earnings_superchat_usd = 0.0
+ membership_count = 0
+ superchat_messages = []
+ start_time = time.time()
+ start_time_const = time.time()
+ if args.earnings:
+ currency_converter = CurrencyConv()
+
+ try:
+ while chat.is_alive():
+ for chat_data in chat.get().sync_items():
+ key = stdscr.getch()
+ if key == ord('q'):
+ raise Exception("User ended data collection")
+ if args.earnings and chat_data.amountValue != 0.0:
+ current_earnings_superchat_usd += currency_converter.convert(chat_data.amountValue, chat_data.currency, "USD")
+ superchat_messages.append((chat_data.author.name, chat_data.amountString, chat_data.message))
+
+ if args.earnings and "Welcome New Member!" in chat_data.message:
+ superchat_messages.append((chat_data.author.name, "Membership", chat_data.message))
+ membership_count += 1
+
+ message_count += 1
+
+ elapsed_time = time.time() - start_time
+ if elapsed_time >= 1:
+ stdscr.erase()
+ messages_per_second = message_count / elapsed_time
+ if args.track_moments > 0 and messages_per_second > args.track_moments:
+ stdscr.addstr(1, 0, "Message rate exceeded " + str(args.track_moments) + " at " + str(time.time()), curses.A_BLINK)
+ if not os.path.exists(f"{video_id}-moments.txt"):
+ open(f"{video_id}-moments.txt", "w").close()
+ with open(f"{video_id}-moments.txt", "a") as f:
+ timestamp = chat_data.timestamp
+ if args.start_time > 0:
+ timestamp = timestamp - args.start_time
+ timestamp = time.strftime("%H:%M:%S", time.gmtime(timestamp))
+
+ f.write(f"Message rate exceeded {args.track_moments} per second at {chat_data.timestamp}\n")
+ messages_per_second_text = f"Messages per second: {messages_per_second:.2f}"
+ timestamp_since_start = time.strftime("%H:%M:%S", time.gmtime(time.time() - start_time_const))
+ info_msg = "Now collecting data for " + video_id + " Elapsed time: " + timestamp_since_start + " seconds (Press q to quit)"
+ chat_window_seperator_txt = "-" * width
+ stdscr.addstr(0, 0, info_msg, curses.A_BOLD)
+ stdscr.addstr(2, 0, messages_per_second_text)
+ stdscr.addstr(6, 0, chat_window_seperator_txt)
+
+ if args.earnings:
+ current_earnings_usd = current_earnings_superchat_usd + membership_count * args.membership
+ earnings_text = f"Current earnings (USD): {current_earnings_usd:.2f}"
+ membership_count_text = f"New/Renewing Members: {membership_count}"
+ stdscr.addstr(3, 0, earnings_text)
+ stdscr.addstr(4, 0, membership_count_text)
+
+ stdscr.refresh()
+ message_count = 0
+ start_time = time.time()
+
+ # Update chat window
+ chat_window.erase()
+ for i, message in enumerate(superchat_messages[max_num_messages:]):
+ chat_window.addstr(i, 0, f"{message[0]}: {message[1]} - {message[2]}")
+ chat_window.refresh()
+
+ except Exception as e:
+ with open(f"{video_id}.txt", "w") as f:
+ f.write(f"Data collection ended due to: {str(e)}\n")
+ f.write(f"Messages per second: {messages_per_second:.2f}\n")
+ if args.earnings:
+ f.write(f"Current earnings (USD): {current_earnings_usd:.2f}\n")
+ f.write(f"New/Renewing Members: {membership_count}\n")
+ f.write("--- Logged Superchats ---\n")
+ for message in superchat_messages:
+ f.write(f"{message[0]}: {message[1]} - {message[2]}\n")
+ print("Saving data collected so far to " + f"{video_id}.txt")
+ print(e)
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description='YouTube Live Chat Message Rate Tracker')
+ parser.add_argument('video_id', help='The ID of the YouTube video for the live chat')
+ parser.add_argument('--earnings', action='store_true', help='Show earnings through Superchats, Super Stickers, and Memberships')
+ parser.add_argument('--membership', type=float, default=4.99, help='The amount of money a membership is worth')
+ parser.add_argument('--track-moments', type=int, default=0, help='Log timestamps when the message rate exceeds this value')
+ parser.add_argument('--max-messages', type=int, default=None, help="The maximum number of messages to collect")
+ parser.add_argument('-start-time', type=int, default=0, help='The known UNIX timestamp of when the stream started (for calculating timestamps')
+ args = parser.parse_args()
+ curses.wrapper(main, args.video_id, args)
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage