Initial commit
Signed-off-by: Abdulkadir Furkan Şanlı <me@abdulocra.cy>
This commit is contained in:
		
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					*~
 | 
				
			||||||
 | 
					vars.env
 | 
				
			||||||
							
								
								
									
										10
									
								
								env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								env.example
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					# Matrix homeserver URL.
 | 
				
			||||||
 | 
					MATRIX_SERVER = ""
 | 
				
			||||||
 | 
					# Matrix room to monitor.
 | 
				
			||||||
 | 
					MATRIX_ROOM = ""
 | 
				
			||||||
 | 
					# Username for bot's Matrix user.
 | 
				
			||||||
 | 
					MATRIX_USER = ""
 | 
				
			||||||
 | 
					# Password for bot's Matrix user.
 | 
				
			||||||
 | 
					MATRIX_PASSWORD = ""
 | 
				
			||||||
 | 
					# Path of sqlite3 file to use.
 | 
				
			||||||
 | 
					DB_PATH = ""
 | 
				
			||||||
							
								
								
									
										218
									
								
								main.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										218
									
								
								main.py
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,218 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/env python3
 | 
				
			||||||
 | 
					"""parkerbot: Matrix bot to generate YouTube (music) playlists from links sent to a channel."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import re
 | 
				
			||||||
 | 
					import sqlite3
 | 
				
			||||||
 | 
					import asyncio
 | 
				
			||||||
 | 
					import pickle
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
 | 
					from dotenv import load_dotenv
 | 
				
			||||||
 | 
					from google_auth_oauthlib.flow import InstalledAppFlow
 | 
				
			||||||
 | 
					from googleapiclient.discovery import build
 | 
				
			||||||
 | 
					from google.auth.transport.requests import Request
 | 
				
			||||||
 | 
					from nio import AsyncClient, MatrixRoom, RoomMessageText
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					load_dotenv()
 | 
				
			||||||
 | 
					DB_PATH = os.getenv("DB_PATH")
 | 
				
			||||||
 | 
					MATRIX_SERVER = os.getenv("MATRIX_SERVER")
 | 
				
			||||||
 | 
					MATRIX_ROOM = os.getenv("MATRIX_ROOM")
 | 
				
			||||||
 | 
					MATRIX_USER = os.getenv("MATRIX_USER")
 | 
				
			||||||
 | 
					MATRIX_PASSWORD = os.getenv("MATRIX_PASSWORD")
 | 
				
			||||||
 | 
					PLAYLIST_TITLE = os.getenv("PLAYLIST_TITLE")
 | 
				
			||||||
 | 
					YOUTUBE_CLIENT_SECRETS_FILE = os.getenv("YOUTUBE_CLIENT_SECRETS_FILE")
 | 
				
			||||||
 | 
					YOUTUBE_API_SERVICE_NAME = "youtube"
 | 
				
			||||||
 | 
					YOUTUBE_API_VERSION = "v3"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					conn = sqlite3.connect(DB_PATH)
 | 
				
			||||||
 | 
					cursor = conn.cursor()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_tables():
 | 
				
			||||||
 | 
					    with conn:
 | 
				
			||||||
 | 
					        cursor.execute(
 | 
				
			||||||
 | 
					            """CREATE TABLE IF NOT EXISTS messages (
 | 
				
			||||||
 | 
					                id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					                sender TEXT,
 | 
				
			||||||
 | 
					                message TEXT,
 | 
				
			||||||
 | 
					                timestamp DATETIME,
 | 
				
			||||||
 | 
					                UNIQUE (sender, message, timestamp))"""
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        cursor.execute(
 | 
				
			||||||
 | 
					            """CREATE TABLE IF NOT EXISTS playlists (
 | 
				
			||||||
 | 
					                id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					                title TEXT,
 | 
				
			||||||
 | 
					                playlist_id TEXT UNIQUE,
 | 
				
			||||||
 | 
					                creation_date DATE)"""
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        cursor.execute(
 | 
				
			||||||
 | 
					            """CREATE TABLE IF NOT EXISTS playlist_tracks (
 | 
				
			||||||
 | 
					                id INTEGER PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
 | 
					                playlist_id INTEGER,
 | 
				
			||||||
 | 
					                message_id INTEGER,
 | 
				
			||||||
 | 
					                FOREIGN KEY (playlist_id) REFERENCES playlists(id),
 | 
				
			||||||
 | 
					                FOREIGN KEY (message_id) REFERENCES messages(id),
 | 
				
			||||||
 | 
					                UNIQUE (playlist_id, message_id))"""
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_authenticated_service():
 | 
				
			||||||
 | 
					    credentials = None
 | 
				
			||||||
 | 
					    # The file token.pickle stores the user's access and refresh tokens.
 | 
				
			||||||
 | 
					    if os.path.exists("token.pickle"):
 | 
				
			||||||
 | 
					        with open("token.pickle", "rb") as token:
 | 
				
			||||||
 | 
					            credentials = pickle.load(token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # If there are no valid credentials available, let the user log in.
 | 
				
			||||||
 | 
					    if not credentials or not credentials.valid:
 | 
				
			||||||
 | 
					        if credentials and credentials.expired and credentials.refresh_token:
 | 
				
			||||||
 | 
					            credentials.refresh(Request())
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            flow = InstalledAppFlow.from_client_secrets_file(
 | 
				
			||||||
 | 
					                YOUTUBE_CLIENT_SECRETS_FILE,
 | 
				
			||||||
 | 
					                scopes=["https://www.googleapis.com/auth/youtube.force-ssl"],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            credentials = flow.run_local_server(port=8080)
 | 
				
			||||||
 | 
					            # Save the credentials for the next run
 | 
				
			||||||
 | 
					            with open("token.pickle", "wb") as token:
 | 
				
			||||||
 | 
					                pickle.dump(credentials, token)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return build("youtube", "v3", credentials=credentials)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_monday_date():
 | 
				
			||||||
 | 
					    today = datetime.now()
 | 
				
			||||||
 | 
					    return today - timedelta(days=today.weekday())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def create_playlist(youtube, title):
 | 
				
			||||||
 | 
					    response = (
 | 
				
			||||||
 | 
					        youtube.playlists()
 | 
				
			||||||
 | 
					        .insert(
 | 
				
			||||||
 | 
					            part="snippet,status",
 | 
				
			||||||
 | 
					            body={
 | 
				
			||||||
 | 
					                "snippet": {
 | 
				
			||||||
 | 
					                    "title": title,
 | 
				
			||||||
 | 
					                    "description": "Weekly playlist created by ParkerBot",
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                "status": {"privacyStatus": "public"},
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute()
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return response["id"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def get_or_create_playlist(youtube, monday_date):
 | 
				
			||||||
 | 
					    playlist_title = f"{PLAYLIST_TITLE} {monday_date.strftime('%Y-%m-%d')}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check if playlist exists in the database
 | 
				
			||||||
 | 
					    cursor.execute(
 | 
				
			||||||
 | 
					        "SELECT playlist_id FROM playlists WHERE title = ?", (playlist_title,)
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    row = cursor.fetchone()
 | 
				
			||||||
 | 
					    if row:
 | 
				
			||||||
 | 
					        return row[0]  # Playlist already exists
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # If not, create a new playlist on YouTube and save it in the database
 | 
				
			||||||
 | 
					    playlist_id = create_playlist(youtube, playlist_title)
 | 
				
			||||||
 | 
					    with conn:
 | 
				
			||||||
 | 
					        cursor.execute(
 | 
				
			||||||
 | 
					            "INSERT INTO playlists (title, playlist_id, creation_date) VALUES (?, ?, ?)",
 | 
				
			||||||
 | 
					            (playlist_title, playlist_id, monday_date),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return playlist_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def add_video_to_playlist(youtube, playlist_id, video_id):
 | 
				
			||||||
 | 
					    youtube.playlistItems().insert(
 | 
				
			||||||
 | 
					        part="snippet",
 | 
				
			||||||
 | 
					        body={
 | 
				
			||||||
 | 
					            "snippet": {
 | 
				
			||||||
 | 
					                "playlistId": playlist_id,
 | 
				
			||||||
 | 
					                "resourceId": {"kind": "youtube#video", "videoId": video_id},
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    ).execute()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async def message_callback(room, event):
 | 
				
			||||||
 | 
					    youtube_link_pattern = r"(https?://(?:www\.|music\.)?youtube\.com/(?!playlist\?list=)watch\?v=[\w-]+|https?://youtu\.be/[\w-]+)"
 | 
				
			||||||
 | 
					    if event.sender != MATRIX_USER:
 | 
				
			||||||
 | 
					        sender = event.sender
 | 
				
			||||||
 | 
					        message_body = event.body
 | 
				
			||||||
 | 
					        room_id = room.room_id
 | 
				
			||||||
 | 
					        monday_date = get_monday_date()
 | 
				
			||||||
 | 
					        youtube = get_authenticated_service()
 | 
				
			||||||
 | 
					        playlist_id = get_or_create_playlist(youtube, monday_date)
 | 
				
			||||||
 | 
					        youtube_links = re.findall(youtube_link_pattern, message_body)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if message_body == "!pow":
 | 
				
			||||||
 | 
					            playlist_link = f"https://www.youtube.com/playlist?list={playlist_id}"
 | 
				
			||||||
 | 
					            reply_msg = f"{sender}, here's the playlist of the week: {playlist_link}"
 | 
				
			||||||
 | 
					            await client.room_send(
 | 
				
			||||||
 | 
					                room_id=room_id,
 | 
				
			||||||
 | 
					                message_type="m.room.message",
 | 
				
			||||||
 | 
					                content={
 | 
				
			||||||
 | 
					                    "msgtype": "m.text",
 | 
				
			||||||
 | 
					                    "body": reply_msg
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if youtube_links:
 | 
				
			||||||
 | 
					            timestamp = event.server_timestamp
 | 
				
			||||||
 | 
					            for link in youtube_links:
 | 
				
			||||||
 | 
					                try:
 | 
				
			||||||
 | 
					                    cursor.execute(
 | 
				
			||||||
 | 
					                        "INSERT INTO messages (sender, message, timestamp) VALUES (?, ?, ?)",
 | 
				
			||||||
 | 
					                        (sender, link, timestamp),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    conn.commit()
 | 
				
			||||||
 | 
					                    print(f"Saved YouTube link from {sender}: {link}")
 | 
				
			||||||
 | 
					                except sqlite3.IntegrityError as e:
 | 
				
			||||||
 | 
					                    if "UNIQUE constraint failed" in str(e):
 | 
				
			||||||
 | 
					                        print(f"Entry already exists: {sender} {link} {timestamp}")
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        raise e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for link in youtube_links:
 | 
				
			||||||
 | 
					                video_id = link.split("v=")[-1]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Check if the link is already added to any playlist
 | 
				
			||||||
 | 
					                cursor.execute("SELECT id FROM messages WHERE message = ?", (link,))
 | 
				
			||||||
 | 
					                message_row = cursor.fetchone()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if message_row:
 | 
				
			||||||
 | 
					                    cursor.execute(
 | 
				
			||||||
 | 
					                        "SELECT id FROM playlist_tracks WHERE message_id = ? AND playlist_id = ?",
 | 
				
			||||||
 | 
					                        (message_row[0], playlist_id),
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    track_row = cursor.fetchone()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if track_row:
 | 
				
			||||||
 | 
					                        print(f"Track already in playlist: {link}")
 | 
				
			||||||
 | 
					                    else:
 | 
				
			||||||
 | 
					                        # Add video to playlist and record it in the database
 | 
				
			||||||
 | 
					                        add_video_to_playlist(youtube, playlist_id, video_id)
 | 
				
			||||||
 | 
					                        with conn:
 | 
				
			||||||
 | 
					                            cursor.execute(
 | 
				
			||||||
 | 
					                                "INSERT INTO playlist_tracks (playlist_id, message_id) VALUES (?, ?)",
 | 
				
			||||||
 | 
					                                (playlist_id, message_row[0]),
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                        print(f"Added track to playlist: {link}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async def main():
 | 
				
			||||||
 | 
					    create_tables()
 | 
				
			||||||
 | 
					    global client
 | 
				
			||||||
 | 
					    client = AsyncClient(MATRIX_SERVER, MATRIX_USER)
 | 
				
			||||||
 | 
					    client.add_event_callback(message_callback, RoomMessageText)
 | 
				
			||||||
 | 
					    print(await client.login(MATRIX_PASSWORD))
 | 
				
			||||||
 | 
					    await client.join(MATRIX_ROOM)
 | 
				
			||||||
 | 
					    await client.sync_forever(timeout=10000)  # milliseconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    asyncio.run(main())
 | 
				
			||||||
							
								
								
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					matrix-nio
 | 
				
			||||||
 | 
					google-auth-oauthlib
 | 
				
			||||||
 | 
					google-api-python-client
 | 
				
			||||||
 | 
					python-dotenv
 | 
				
			||||||
		Reference in New Issue
	
	Block a user