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