sync-mpv/sync_mpv_server.py

290 lines
8.1 KiB
Python
Raw Permalink Normal View History

2022-04-08 09:20:44 -06:00
#!/usr/bin/env python
2023-09-14 10:46:17 -06:00
"""
sync_mpv_server.py
Copyright 2022, 2023 <midnightman@protonmail.com>
Copyright 2023, Jeff Moe <moe@spacecruft.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
2023-09-13 14:21:19 -06:00
from Cryptodome.Cipher import AES
from Cryptodome.Util.Padding import pad, unpad
2022-04-08 09:20:44 -06:00
from configparser import ConfigParser
import socket
import select
import hashlib
import sys
import threading
import errno
import time
import os
2023-09-14 11:15:53 -06:00
import argparse
# Parse command line options
parser = argparse.ArgumentParser(description="Run mpv synchronization Server")
parser.add_argument(
"-i",
"--ip",
help="Server IP address (default 0.0.0.0)",
type=str,
required=False,
default="0.0.0.0",
)
parser.add_argument(
"-p",
"--port",
help="Server network port (default 51984)",
type=int,
required=False,
default="51984",
)
parser.add_argument("-u", "--url", help="URL to play", type=str, required=False)
args = parser.parse_args()
IP = args.ip
PORT = args.port
URL = args.url
2022-04-08 09:20:44 -06:00
HEADER_LENGTH = 32
2023-09-14 10:51:07 -06:00
FORMAT = "utf-8"
DISCONNECT_MESSAGE = "!DISCONNECT"
2022-04-08 09:20:44 -06:00
2023-09-14 10:51:07 -06:00
def prepare_concatenation(msg):
2022-04-08 09:20:44 -06:00
concat = str(msg).encode("utf-8")
2023-09-14 10:51:07 -06:00
concat += b" " * (HEADER_LENGTH - len(concat))
2022-04-08 09:20:44 -06:00
return concat
2023-09-14 10:51:07 -06:00
def send(clientsocket, msg):
2022-04-08 09:20:44 -06:00
global KEY
cipher = AES.new(KEY, AES.MODE_CBC)
if type(msg) is not bytes:
msg = msg.encode("utf-8")
msg = encrypt_message(msg)
send_length = prepare_concatenation(len(msg))
clientsocket.sendall(send_length)
clientsocket.sendall(msg)
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def encrypt_message(msg):
global KEY
cipher = AES.new(KEY, AES.MODE_CBC)
2023-09-14 10:51:07 -06:00
encrypted_msg = cipher.encrypt(pad(msg, AES.block_size))
2022-04-08 09:20:44 -06:00
encrypted_msg = cipher.iv + encrypted_msg
return encrypted_msg
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def decrypt_message(msg, IV):
global KEY
cipher = AES.new(KEY, AES.MODE_CBC, IV)
decrypted_msg = unpad(cipher.decrypt(msg), AES.block_size)
try:
decrypted_msg = decrypted_msg.decode("utf-8")
except UnicodeDecodeError:
pass
return decrypted_msg
2023-09-14 10:51:07 -06:00
def receive_message(client_socket):
2022-04-08 09:20:44 -06:00
try:
message_header = client_socket.recv(HEADER_LENGTH)
except:
return False
message_length = int(message_header)
msg = client_socket.recv(message_length)
decrypted_msg = decrypt_message(msg[16:], msg[:16])
return decrypted_msg
2023-09-14 10:51:07 -06:00
def parse_config(parser, configfile):
2022-04-08 09:20:44 -06:00
parser.read(configfile)
2023-09-14 10:51:07 -06:00
return parser.get("connection", "password")
2022-04-08 09:20:44 -06:00
def initialize(parser, configfile):
2023-09-14 10:51:07 -06:00
print(
"Decide for a 16-digit password.\nPassword will be saved under '~/.config/sync-mpv/serverpassword.conf'\n"
)
2022-04-08 09:20:44 -06:00
while True:
PASSWORD = input("Password: ")
if len(PASSWORD) == 16:
break
print(f"\nPassword is {len(PASSWORD)} digits long. Choose another.")
2023-09-14 10:51:07 -06:00
parser["connection"] = {
"password": "%s" % PASSWORD,
2022-04-08 09:20:44 -06:00
}
2023-09-14 10:51:07 -06:00
with open(configfile, "w") as f:
2022-04-08 09:20:44 -06:00
parser.write(f)
return PASSWORD
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
def main():
global KEY
configfolder = os.path.expanduser("~/.config/sync-mpv/")
configfile = os.path.expanduser("~/.config/sync-mpv/serverpassword.conf")
parser = ConfigParser()
if os.path.exists(configfolder):
pass
else:
os.mkdir(configfolder)
if os.path.exists(configfile):
PASSWORD = parse_config(parser, configfile)
else:
PASSWORD = initialize(parser, configfile)
KEY = hashlib.sha256(PASSWORD.encode()).digest()
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((IP, PORT))
server_socket.listen()
sockets_list = [server_socket]
clients = {}
2023-09-14 10:51:07 -06:00
print(f"Listening for connections on {IP}:{PORT}...")
2022-04-08 09:20:44 -06:00
readycounter = 0
2023-09-14 11:15:53 -06:00
last_video = URL
2022-04-08 09:20:44 -06:00
while True:
2023-09-14 10:51:07 -06:00
read_sockets, _, exception_sockets = select.select(
sockets_list, [], sockets_list
)
2022-04-08 09:20:44 -06:00
for notified_socket in read_sockets:
# If notified socket is a server socket - new connection, accept it
if notified_socket == server_socket:
readycounter = 0
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
client_socket, client_address = server_socket.accept()
user = receive_message(client_socket)
# If False - client disconnected before he sent his name
2023-09-14 10:51:07 -06:00
if user is False or user == b"":
2022-04-08 09:20:44 -06:00
continue
sockets_list.append(client_socket)
clients[client_socket] = user
2023-09-14 10:51:07 -06:00
print(
"Accepted new connection from {}:{}, username: {}".format(
*client_address, user
)
)
2022-04-08 09:20:44 -06:00
for client in clients:
send(client, f"number of clients {len(clients)}")
if client != server_socket and client != client_socket:
send(client, f"userconnected {user}")
if last_video is not None:
send(client, f"mpv new {last_video}")
# Else existing socket is sending a message
else:
try:
message = receive_message(notified_socket)
# If False, client disconnected, cleanup
except:
2023-09-14 10:51:07 -06:00
# print('Closed connection from: {}'.format(clients[notified_socket]))
# sockets_list.remove(notified_socket)
2022-04-08 09:20:44 -06:00
2023-09-14 10:51:07 -06:00
# del clients[notified_socket]
# readycounter = 0
2022-04-08 09:20:44 -06:00
2023-09-14 10:51:07 -06:00
# for client in clients:
2022-04-08 09:20:44 -06:00
# send(client, "number of clients %s"%len(clients))
continue
if message != False:
2023-09-14 10:51:07 -06:00
# Get user by notified socket, so we will know who sent the message
2022-04-08 09:20:44 -06:00
user = clients[notified_socket]
2023-09-14 10:51:07 -06:00
print(f"Received message from {user}: {message}")
2022-04-08 09:20:44 -06:00
if "mpv new" in message:
readycounter = 0
last_video = message[8:]
2023-09-14 10:51:07 -06:00
# if "mpv skip" in message:
2022-04-08 09:20:44 -06:00
# readycounter = 0
if "ready" in message:
print(len(clients))
readycounter += 1
print("readycounter ", readycounter)
2023-09-14 10:51:07 -06:00
else:
2022-04-08 09:20:44 -06:00
readycounter = 0
if message == "!DISCONNECT":
for client_socket in clients:
if client_socket != notified_socket:
2023-09-14 10:51:07 -06:00
send(
client_socket,
f"{clients[notified_socket]} disconnected. , {user}",
)
2022-04-08 09:20:44 -06:00
send(notified_socket, message)
sockets_list.remove(notified_socket)
del clients[notified_socket]
if readycounter == len(clients):
for client_socket in clients:
send(client_socket, "mpv playback")
readycounter = 0
for client_socket in clients:
if "!DISCONNECT" != message:
if client_socket != notified_socket:
send(client_socket, f"{message} , {user}")
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
# It's not really necessary to have this, but will handle some socket exceptions just in case
for notified_socket in exception_sockets:
# Remove from list for socket.socket()
sockets_list.remove(notified_socket)
# Remove from our list of users
del clients[notified_socket]
2023-09-14 10:51:07 -06:00
2022-04-08 09:20:44 -06:00
if __name__ == "__main__":
main()