Had to start the push over
This commit is contained in:
commit
a413e70928
15
.github/workflows/discord_sync.yml
vendored
Normal file
15
.github/workflows/discord_sync.yml
vendored
Normal 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
165
.gitignore
vendored
Normal 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
15
.vscode/launch.json
vendored
Normal 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
287
main.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user