commit a413e7092812cba6868cd25956d39c7681230cdd Author: Dan Date: Mon Aug 19 19:44:30 2024 -0400 Had to start the push over diff --git a/.github/workflows/discord_sync.yml b/.github/workflows/discord_sync.yml new file mode 100644 index 0000000..6a92ee8 --- /dev/null +++ b/.github/workflows/discord_sync.yml @@ -0,0 +1,15 @@ +name: Discord Webhook + +on: [push] + +jobs: + git: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v2 + + - name: Run Discord Webhook + uses: johnnyhuy/actions-discord-git-webhook@main + with: + webhook_url: ${{ secrets.YOUR_DISCORD_WEBHOOK_URL }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f9fe8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,165 @@ +# 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/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# 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/ +/client_secret.json +/token.json +/channel_points.db \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ac4dca4 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "E:\\Development\\Kenny Projects\\YoutubeBot\\main.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..d5d29db --- /dev/null +++ b/main.py @@ -0,0 +1,287 @@ +import os +import sqlite3 +import time +from google.oauth2.credentials import Credentials +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request +from googleapiclient.discovery import build +from datetime import datetime, timedelta, timezone + +# Constants +SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'] +DATABASE_FILE = 'channel_points.db' + +# YouTube Channel ID or Handle +CHANNEL_HANDLE = 'UCsVJcf4KbO8Vz308EKpSYxw' + + +def get_authenticated_service(): + flow = InstalledAppFlow.from_client_secrets_file( + 'client_secret.json', SCOPES) + creds = flow.run_local_server(port=63355) + with open('token.json', 'w') as token: + token.write(creds.to_json()) + return build('youtube', 'v3', credentials=creds) + + +def create_database(): + conn = sqlite3.connect(DATABASE_FILE) + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS points ( + user_id TEXT PRIMARY KEY, + points INTEGER DEFAULT 0, + last_interaction TIMESTAMP, + subscription_status TEXT, + first_seen_as_member TIMESTAMP + ) + ''') + conn.commit() + conn.close() + + +def add_points(user_id, points_to_add, subscription_status, interacted): + conn = sqlite3.connect(DATABASE_FILE) + cursor = conn.cursor() + + # Determine the multiplier based on subscription status + if subscription_status == "year_or_more": + bonus_multiplier = 3 + elif subscription_status == "subscribed": + bonus_multiplier = 2 + else: + bonus_multiplier = 1 + + if interacted: + points_to_add += 5 # 5 extra points for interaction + + points_to_add *= bonus_multiplier + + cursor.execute(''' + INSERT INTO points (user_id, points) + VALUES (?, ?) + ON CONFLICT(user_id) DO UPDATE SET points = points + ?, last_interaction = ? + ''', (user_id, points_to_add, points_to_add, datetime.utcnow())) + conn.commit() + conn.close() + + print(f"User {user_id} earned {points_to_add} points. Subscription status: {subscription_status}. Multiplier applied: {bonus_multiplier}") + + +def get_channel_id(youtube, handle): + request = youtube.channels().list( + part="id", + forUsername=handle if handle.startswith("@") else None, + id=handle if not handle.startswith("@") else None + ) + response = request.execute() + items = response.get('items', []) + return items[0]['id'] if items else None + + +def get_channel_uploads_playlist_id(youtube, channel_id): + request = youtube.channels().list( + part="contentDetails", + id=channel_id + ) + response = request.execute() + items = response.get('items', []) + if items: + return items[0]['contentDetails']['relatedPlaylists']['uploads'] + return None + + +def get_latest_video_id_from_playlist(youtube, playlist_id): + request = youtube.playlistItems().list( + part="snippet", + playlistId=playlist_id, + maxResults=1 + ) + response = request.execute() + items = response.get('items', []) + if items: + return items[0]['snippet']['resourceId']['videoId'] + return None + + +def is_video_live(youtube, video_id): + request = youtube.videos().list( + part="snippet,liveStreamingDetails", + id=video_id + ) + response = request.execute() + items = response.get('items', []) + if not items: + return False + + snippet = items[0]['snippet'] + live_details = items[0].get('liveStreamingDetails', {}) + + # Ensure the video is currently live + if snippet.get('liveBroadcastContent') == 'live': + actual_start_time = live_details.get('actualStartTime') + actual_end_time = live_details.get('actualEndTime') + + if actual_start_time and not actual_end_time: + return True + + return False + + +def get_live_chat_id(youtube, video_id): + request = youtube.videos().list( + part="liveStreamingDetails", + id=video_id + ) + response = request.execute() + items = response.get('items', []) + if items: + return items[0]['liveStreamingDetails'].get('activeLiveChatId') + return None + + +def monitor_chat(youtube, live_chat_id): + if not live_chat_id: + print("No valid live chat ID found.") + return False + + next_page_token = None + + while True: + try: + request = youtube.liveChatMessages().list( + liveChatId=live_chat_id, + part="snippet,authorDetails", + maxResults=200, + pageToken=next_page_token + ) + response = request.execute() + + if 'items' in response and response['items']: + for item in response['items']: + user_id = item['authorDetails']['channelId'] + display_name = item['authorDetails']['displayName'] + is_moderator = item['authorDetails']['isChatModerator'] + is_member = item['authorDetails']['isChatSponsor'] # Paid membership badge + member_since = item['authorDetails'].get('memberSince', None) # Member since date (if available) + message = item['snippet']['displayMessage'] + published_time = datetime.strptime(item['snippet']['publishedAt'], '%Y-%m-%dT%H:%M:%S.%f%z') + + print(f"[{published_time}] {display_name}: {message} | Member: {is_member} | Member Since: {member_since}") + + if not is_moderator: + conn = sqlite3.connect(DATABASE_FILE) + cursor = conn.cursor() + cursor.execute("SELECT last_interaction, subscription_status, first_seen_as_member FROM points WHERE user_id = ?", (user_id,)) + result = cursor.fetchone() + + if result: + last_interaction, subscription_status, first_seen_as_member = result + + if isinstance(first_seen_as_member, str): + first_seen_as_member = datetime.fromisoformat(first_seen_as_member) + + if first_seen_as_member is None and is_member: + first_seen_as_member = published_time + cursor.execute(''' + UPDATE points + SET first_seen_as_member = ? + WHERE user_id = ? + ''', (first_seen_as_member, user_id)) + conn.commit() + + if first_seen_as_member and is_member: + membership_duration = datetime.now(timezone.utc) - first_seen_as_member + if membership_duration.days >= 365: + subscription_status = "year_or_more" + else: + subscription_status = "subscribed" + + else: + if is_member: + subscription_status = "subscribed" + first_seen_as_member = published_time + else: + subscription_status = "none" + first_seen_as_member = None + + cursor.execute(''' + INSERT INTO points (user_id, points, last_interaction, subscription_status, first_seen_as_member) + VALUES (?, 0, NULL, ?, ?) + ''', (user_id, subscription_status, first_seen_as_member)) + conn.commit() + + print(f"User {user_id} subscription status set to: {subscription_status}, member_since: {first_seen_as_member}") + + interacted = True + + if interacted: + add_points(user_id, 10, subscription_status, interacted) + + conn.close() + + next_page_token = response.get('nextPageToken') + + else: + print("No new messages detected; continuing to poll...") + + except Exception as e: + print(f"Error while monitoring chat: {e}") + + time.sleep(10) # Adjust this delay as needed + + +def set_membership_duration(user_id, months): + """Manually set a user's membership duration.""" + conn = sqlite3.connect(DATABASE_FILE) + cursor = conn.cursor() + + # Calculate the membership start date + start_date = datetime.now(timezone.utc) - timedelta(days=months * 30) + + cursor.execute(''' + UPDATE points + SET first_seen_as_member = ?, subscription_status = ? + WHERE user_id = ? + ''', (start_date, "year_or_more" if months >= 12 else "subscribed", user_id)) + + conn.commit() + conn.close() + + print(f"Manually set {user_id}'s membership start date to {start_date} ({months} months ago).") + + +def main(): + youtube = get_authenticated_service() + create_database() + + # Example manual update + set_membership_duration("UCfAxcCBuGbLqo-OjPr690Jg", 9) # Example user with 9 months of membership + + channel_id = get_channel_id(youtube, CHANNEL_HANDLE) + if not channel_id: + print("Channel ID not found!") + return + + playlist_id = get_channel_uploads_playlist_id(youtube, channel_id) + if not playlist_id: + print("Uploads playlist not found!") + return + + while True: + video_id = get_latest_video_id_from_playlist(youtube, playlist_id) + if video_id and is_video_live(youtube, video_id): + print("Channel is live!") + live_chat_id = get_live_chat_id(youtube, video_id) + if live_chat_id: + print("Monitoring chat...") + monitor_chat(youtube, live_chat_id) + else: + print("No live chat ID available.") + else: + print("Channel is not live.") + time.sleep(300) # Check every 5 minutes + + +if __name__ == "__main__": + main()