Had to start the push over

This commit is contained in:
Dan 2024-08-19 19:44:30 -04:00
commit a413e70928
4 changed files with 482 additions and 0 deletions

15
.github/workflows/discord_sync.yml vendored Normal file
View File

@ -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 }}

165
.gitignore vendored Normal file
View File

@ -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

15
.vscode/launch.json vendored Normal file
View File

@ -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"
}
]
}

287
main.py Normal file
View File

@ -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()