diff --git a/main.py b/main.py index 1e53e78..35d3fdf 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,8 @@ import src.win.screen from src.utils import user_setting import src.discord.client -import locale +import src.with_play +import locale, threading from pypresence.exceptions import DiscordNotFound args = sys.argv[1:] @@ -11,6 +12,7 @@ nogui_op = False once_op = False playlist_op = False + for arg in args: if not arg.startswith("--") and playlist_op: src.win.setting.video_list.append(arg) @@ -22,6 +24,10 @@ once_op = True if arg == "--play": playlist_op = True + if arg == "--with-play-server": + src.with_play.Start_Server() + if arg == "--with-play-client": + src.with_play.Start_Client('192.168.0.101') try: if user_setting.discord_RPC: diff --git a/requirements.txt b/requirements.txt index 96f5e4b..1c24a44 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ pygame == 2.5.2 yt_dlp == 2024.12.13 moviepy == 1.0.3 chardet == 5.2.0 +requests == 2.32.3 pytubefix == 7.1rc2 pyvidplayer2 == 0.9.24 opencv-python == 4.5.5.64 diff --git a/server.json b/server.json new file mode 100644 index 0000000..207cc72 --- /dev/null +++ b/server.json @@ -0,0 +1,3 @@ +{ + "max-client": 5 +} \ No newline at end of file diff --git a/src/gui.py b/src/gui.py index d1c8be4..f75c09b 100644 --- a/src/gui.py +++ b/src/gui.py @@ -1,4 +1,7 @@ import chardet, cv2, time, re, os + +import src.socket +import src.socket.client os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1' import pygame, pygame.scrap from pyvidplayer2 import Video @@ -11,6 +14,8 @@ import src.win.screen import src.win.setting import src.discord.client +import src.with_play +import src.socket.server # Global state @dataclass @@ -199,6 +204,10 @@ def run(url: str): state.msg_text = "" if user_setting.discord_RPC: src.discord.client.update(time.time(),src.win.screen.vid.name) + if src.with_play.server: + src.socket.server.seek = 0 + src.socket.server.playurl = url + src.socket.server.broadcast_message({"type":"play-info","playurl": url,"seek": 0}) while src.win.screen.vid.active: key = None for event in pygame.event.get(): @@ -279,133 +288,151 @@ def wait(once): pygame.key.set_text_input_rect(pygame.Rect(0, 0, 0, 0)) if user_setting.discord_RPC: src.discord.client.update(time.time(),"waiting...") + if src.with_play.server: + src.socket.server.seek = 0 + src.socket.server.playurl = '' + src.socket.server.broadcast_message({"type":"play-wait"}) + last_playinfo_time = time.time() while True: src.win.screen.win.fill((0, 0, 0)) - key = None - if len(src.win.setting.video_list) == 0: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.display.quit() - pygame.quit() - return - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - if user_setting.discord_RPC: - src.discord.client.RPC.close() - pygame.quit() - exit(0) - elif event.key == pygame.K_BACKSPACE: - state.search = state.search[:-1] - elif event.key == pygame.K_RETURN: - key = "return" - elif event.key == pygame.K_v and (pygame.key.get_mods() & pygame.KMOD_CTRL): - if pygame.scrap.get_init(): - copied_text = pygame.scrap.get(pygame.SCRAP_TEXT) - if copied_text: - try: - copied_text = copied_text.decode('utf-8').strip('\x00') - except UnicodeDecodeError: - detected = chardet.detect(copied_text) - encoding = detected['encoding'] - copied_text = copied_text.decode(encoding).strip('\x00') - state.search += copied_text - elif event.type == pygame.TEXTINPUT: - state.search += event.text - if not key: - text_surface = src.win.screen.font.render(f"search video : {state.search}", True, (255,255,255)) - text_rect = text_surface.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2)) - src.win.screen.win.blit(text_surface, text_rect) + if src.with_play.client: + if src.socket.client.play: pygame.display.update() - continue - elif key == "backspace": - state.search = state.search[0:len(state.search)-1] - elif len(key) == 1: - state.search = state.search + key - text_surface = src.win.screen.font.render(f"search video : {state.search}", True, (255,255,255)) + run(src.socket.client.url) + current_time = time.time() + if current_time - last_playinfo_time >= 1: + src.socket.client.playinfo() + last_playinfo_time = current_time + text_surface = src.win.screen.font.render("Waiting for the server to play the song", True, (255,255,255)) text_rect = text_surface.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2)) src.win.screen.win.blit(text_surface, text_rect) pygame.display.update() - if key == "enter" or key == "return": - if is_playlist(state.search): - video_urls = download.get_playlist_video(state.search) - src.win.setting.video_list.extend(video_urls) - state.search = "" - elif is_url(state.search): - a = state.search - src.win.setting.video_list.append(a) - state.search = "" - else: - src.win.screen.win.fill((0,0,0)) - text_surface = src.win.screen.font.render(f"Searching YouTube videos...", True, (255,255,255)) + else: + key = None + if len(src.win.setting.video_list) == 0: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.display.quit() + pygame.quit() + return + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + if user_setting.discord_RPC: + src.discord.client.RPC.close() + pygame.quit() + exit(0) + elif event.key == pygame.K_BACKSPACE: + state.search = state.search[:-1] + elif event.key == pygame.K_RETURN: + key = "return" + elif event.key == pygame.K_v and (pygame.key.get_mods() & pygame.KMOD_CTRL): + if pygame.scrap.get_init(): + copied_text = pygame.scrap.get(pygame.SCRAP_TEXT) + if copied_text: + try: + copied_text = copied_text.decode('utf-8').strip('\x00') + except UnicodeDecodeError: + detected = chardet.detect(copied_text) + encoding = detected['encoding'] + copied_text = copied_text.decode(encoding).strip('\x00') + state.search += copied_text + elif event.type == pygame.TEXTINPUT: + state.search += event.text + if not key: + text_surface = src.win.screen.font.render(f"search video : {state.search}", True, (255,255,255)) text_rect = text_surface.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2)) src.win.screen.win.blit(text_surface, text_rect) - pygame.display.flip() - load = False - choice = 0 - videos = download.search(state.search,10)[:5] - src.win.screen.win.fill((0,0,0)) - pygame.display.flip() - while True: - key = "" - for event in pygame.event.get(): - if event.type == pygame.QUIT: - pygame.display.quit() - pygame.quit() - return - elif event.type == pygame.KEYDOWN: - key = pygame.key.name(event.key) - if key == "up": - if choice != 0: - choice -= 1 - else: - choice = len(videos) - 1 - elif key == "down": - if choice != len(videos) - 1: - choice += 1 - else: - choice = 0 - elif key == "escape": - src.win.setting.video_list = [] - break + pygame.display.update() + continue + elif key == "backspace": + state.search = state.search[0:len(state.search)-1] + elif len(key) == 1: + state.search = state.search + key + text_surface = src.win.screen.font.render(f"search video : {state.search}", True, (255,255,255)) + text_rect = text_surface.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2)) + src.win.screen.win.blit(text_surface, text_rect) + pygame.display.update() + if key == "enter" or key == "return": + if is_playlist(state.search): + video_urls = download.get_playlist_video(state.search) + src.win.setting.video_list.extend(video_urls) + state.search = "" + elif is_url(state.search): + a = state.search + src.win.setting.video_list.append(a) + state.search = "" + else: + src.win.screen.win.fill((0,0,0)) + text_surface = src.win.screen.font.render(f"Searching YouTube videos...", True, (255,255,255)) + text_rect = text_surface.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2)) + src.win.screen.win.blit(text_surface, text_rect) + pygame.display.flip() + load = False + choice = 0 + videos = download.search(state.search,10)[:5] src.win.screen.win.fill((0,0,0)) - for i, video in enumerate(videos): - if i == choice: - text_surface = src.win.screen.font.render(video.title, True, (0,0,255)) - else: - text_surface = src.win.screen.font.render(video.title, True, (255,255,255)) - text_rect = text_surface.get_rect() - text_rect.centerx = src.win.screen.win.get_size()[0] // 2 - text_rect.y = i * 30 + 50 - src.win.screen.win.blit(text_surface, text_rect) - if not load: - pygame.display.flip() - load = True pygame.display.flip() - if key == "enter" or key == "return": - src.win.setting.video_list.append(f"https://www.youtube.com/watch?v={videos[choice].watch_url}") + while True: + key = "" + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.display.quit() + pygame.quit() + return + elif event.type == pygame.KEYDOWN: + key = pygame.key.name(event.key) + if key == "up": + if choice != 0: + choice -= 1 + else: + choice = len(videos) - 1 + elif key == "down": + if choice != len(videos) - 1: + choice += 1 + else: + choice = 0 + elif key == "escape": + src.win.setting.video_list = [] + break + src.win.screen.win.fill((0,0,0)) + for i, video in enumerate(videos): + if i == choice: + text_surface = src.win.screen.font.render(video.title, True, (0,0,255)) + else: + text_surface = src.win.screen.font.render(video.title, True, (255,255,255)) + text_rect = text_surface.get_rect() + text_rect.centerx = src.win.screen.win.get_size()[0] // 2 + text_rect.y = i * 30 + 50 + src.win.screen.win.blit(text_surface, text_rect) + if not load: + pygame.display.flip() + load = True + pygame.display.flip() + if key == "enter" or key == "return": + src.win.setting.video_list.append(f"https://www.youtube.com/watch?v={videos[choice].watch_url}") + break + trys = 0 + while len(src.win.setting.video_list) != 0: + try: + run(src.win.setting.video_list[0]) + if once: break - trys = 0 - while len(src.win.setting.video_list) != 0: - try: - run(src.win.setting.video_list[0]) - if once: - break - except Exception as e: - if src.win.screen.vid == None: - src.win.screen.reset((state.search_width, state.search_height)) - else: - src.win.screen.reset((src.win.screen.vid.current_size[0],src.win.screen.vid.current_size[1]+5), vid=True) - if trys >= 10: - print("fail") - src.win.setting.video_list = [] - break - print(f"An error occurred during playback. Trying again... ({trys}/10) > \n{e}") - text_surface = src.win.screen.font.render(f"An error occurred during playback. Trying again... ({trys}/10) >", True, (255,255,255)) - text_surface_2 = src.win.screen.font.render(f"{e}", True, (255,255,255)) - text_rect = text_surface.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2)) - text_rect_2 = text_surface_2.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2+30)) - src.win.screen.win.blit(text_surface, text_rect) - src.win.screen.win.blit(text_surface_2, text_rect_2) - pygame.display.flip() - time.sleep(0.5) - trys += 1 \ No newline at end of file + except Exception as e: + if src.win.screen.vid == None: + src.win.screen.reset((state.search_width, state.search_height)) + else: + src.win.screen.reset((src.win.screen.vid.current_size[0],src.win.screen.vid.current_size[1]+5), vid=True) + if trys >= 10: + print("fail") + src.win.setting.video_list = [] + break + print(f"An error occurred during playback. Trying again... ({trys}/10) > \n{e}") + text_surface = src.win.screen.font.render(f"An error occurred during playback. Trying again... ({trys}/10) >", True, (255,255,255)) + text_surface_2 = src.win.screen.font.render(f"{e}", True, (255,255,255)) + text_rect = text_surface.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2)) + text_rect_2 = text_surface_2.get_rect(center=(src.win.screen.win.get_size()[0]/2,src.win.screen.win.get_size()[1]/2+30)) + src.win.screen.win.blit(text_surface, text_rect) + src.win.screen.win.blit(text_surface_2, text_rect_2) + pygame.display.flip() + time.sleep(0.5) + trys += 1 \ No newline at end of file diff --git a/src/socket/client.py b/src/socket/client.py new file mode 100644 index 0000000..ffc9f29 --- /dev/null +++ b/src/socket/client.py @@ -0,0 +1,41 @@ +import socket, json, time +import src.gui + +client = None +play = False + +url = '' +seek = 0 + +def playinfo(): + try: + data = json.dumps({"type": "req-play-info"}) + client.send(data.encode('utf-8')) + except Exception as e: + return None + +def start_client(server_ip): + global client, play, url, seek + client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + client.connect((server_ip, 12377)) + try: + playinfo() + while True: + time.sleep(2) + response = client.recv(1024) + try: + message_data = json.loads(response.decode('utf-8').split("}{")[0] + "}") + except Exception as e: + message_data = json.loads(response.decode('utf-8')) + type = message_data.get('type') + print(message_data) + if type == "play-info": + play = True + url = message_data.get('playurl') + seek = message_data.get('seek') + if type == "play-wait": + play = False + except KeyboardInterrupt: + pass + finally: + client.close() diff --git a/src/socket/server.py b/src/socket/server.py new file mode 100644 index 0000000..e79175d --- /dev/null +++ b/src/socket/server.py @@ -0,0 +1,58 @@ +import socket +import threading +import json +import src.socket.setting as setting +import src.socket.user as user + +playurl = '' +seek = 0 +clients = [] + +def handle_client(client_socket, client_address, clients): + try: + while True: + data = client_socket.recv(1024) + if not data: + break + + try: + message_data = json.loads(data.decode('utf-8')) + message_type = message_data.get('type') + if message_type == "req-play-info": + if playurl != '': + response = {"type": "play-info", "playurl": playurl, "seek": seek} + else: + response = {"type": "play-wait"} + client_socket.send(json.dumps(response).encode('utf-8')) + except json.JSONDecodeError: + pass + + except Exception as e: + pass + finally: + if client_socket in clients: + clients.remove(client_socket) + client_socket.close() + +def broadcast_message(message): + for client in clients[:]: + try: + data = json.dumps(message) + client.send(data.encode('utf-8')) + except Exception as e: + print(f"클라이언트에게 메시지 전송 중 오류 발생: {e}") + if client in clients: + clients.remove(client) + +def start_server(): + global clients + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.bind(('0.0.0.0', 12377)) + server.listen(setting.max_client) + print(f"SERVER START (ADDRESS) : {user.get_internal_ip()} / {user.get_external_ip()} [NEED PORT FORWARDING -> :12377]") + + while True: + client_socket, client_address = server.accept() + clients.append(client_socket) + client_thread = threading.Thread(target=handle_client, args=(client_socket, client_address, clients)) + client_thread.start() diff --git a/src/socket/setting.py b/src/socket/setting.py new file mode 100644 index 0000000..7691d58 --- /dev/null +++ b/src/socket/setting.py @@ -0,0 +1,23 @@ +import src.utils.json as json +json_file_path = "./server.json" +max_client = 0 +def init_file(): + data = {} + data['max-client'] = 5 + return data +def change_setting_data(key:str,value): + data = json.read(json_file_path) + if data == None: + data = init_file() + data[key] = value + json.write(json_file_path,data) + reload_setting_file() +def reload_setting_file(): + global max_client + data = json.read(json_file_path) + if data == None: + data = init_file() + json.write(json_file_path,data) + max_client = data['max-client'] +##################### +reload_setting_file() \ No newline at end of file diff --git a/src/socket/user.py b/src/socket/user.py new file mode 100644 index 0000000..051c7ac --- /dev/null +++ b/src/socket/user.py @@ -0,0 +1,18 @@ +import socket, requests +def get_external_ip(): + try: + response = requests.get("https://api.ipify.org?format=json") + if response.status_code == 200: + return response.json()['ip'] + else: + return '?:?:?:?' + except requests.RequestException as e: + return '?:?:?:?' + +def get_internal_ip(): + try: + hostname = socket.gethostname() + internal_ip = socket.gethostbyname(hostname) + return internal_ip + except socket.error as e: + return '?:?:?:?' \ No newline at end of file diff --git a/src/with_play.py b/src/with_play.py new file mode 100644 index 0000000..928cc4f --- /dev/null +++ b/src/with_play.py @@ -0,0 +1,20 @@ +import src.socket.server +import src.socket.client +import threading + +client = False +server = False + +def Start_Server(): + global server + server = True + socket_thread = threading.Thread(target=src.socket.server.start_server) + socket_thread.daemon = True + socket_thread.start() + +def Start_Client(server_ip): + global client + client = True + socket_thread = threading.Thread(target=src.socket.client.start_client, args=(server_ip,)) + socket_thread.daemon = True + socket_thread.start() \ No newline at end of file diff --git a/start client.bat b/start client.bat new file mode 100644 index 0000000..aa2f27e --- /dev/null +++ b/start client.bat @@ -0,0 +1 @@ +python ./main.py --with-play-client \ No newline at end of file diff --git a/start server.bat b/start server.bat new file mode 100644 index 0000000..7b7d6f8 --- /dev/null +++ b/start server.bat @@ -0,0 +1 @@ +python ./main.py --with-play-server \ No newline at end of file