initial commit

This commit is contained in:
2025-07-20 00:51:55 +03:00
commit 42684e0cb6
29 changed files with 2615 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
import web.pages.all_rooms as rooms_page
import web.pages.contents as movies_page
import web.pages.index as index_page
import web.pages.room as room_page
+30
View File
@@ -0,0 +1,30 @@
from nicegui import ui
import globals
from web.custom_widgets import draw_header
from web.misc import check_user
async def page():
ui.page_title("Watch With Friends - Rooms")
if not await check_user():
ui.navigate.to("/")
await draw_header()
main_row = ui.row().classes("w-full")
for room in globals.ROOMS_DATABASE.rooms:
with main_row, ui.link(target=f"/room/{room.uid}"), ui.card().style("border-radius: 15px"):
content = globals.MOVIES_DATABASE.by_tmdb_id[room.tmdb_id]
with ui.row(wrap=False):
ui.image(content.poster_url).style("width: 20%")
with ui.column(wrap=False).classes("").style("gap: 0px"):
ui.html(f"{room.uid}")
ui.html(f"<b>{content.title}</b>")
users = [globals.USERS_DATABASE.by_uid[uid] for uid in room.connected_users]
ui.html(f"<i>{", ".join(u.username for u in users)}</i>")
+18
View File
@@ -0,0 +1,18 @@
from nicegui import ui
import globals
from web.custom_widgets import draw_header, ContentCard
from web.misc import check_user
async def page():
ui.page_title("Watch With Friends - Contents")
if not await check_user():
ui.navigate.to("/")
await draw_header()
with ui.row(wrap=True).classes("w-full").style("margin: auto; gap: 16px"):
for content in globals.MOVIES_DATABASE.contents:
ContentCard(content.tmdb_id)
+43
View File
@@ -0,0 +1,43 @@
from nicegui import ui, app
import config
import globals
from users.utils import generate_token
from web.misc import check_user
async def page():
ui.page_title("Watch With Friends")
if not await check_user():
await handle_login()
ui.navigate.to("/contents")
async def handle_login():
async def try_create_user():
login = login_input.value.strip()
password = password_input.value.strip()
if password != config.PASSWORD:
ui.notify("Incorrect password!", type="negative", position="top")
return
new_user = await globals.USERS_DATABASE.create_user(login)
token = generate_token(new_user)
if token:
app.storage.user["token"] = token
dialog.close()
else:
ui.notify("Something went wrong!", type="negative", position="top")
with ui.dialog().props("persistent") as dialog, ui.card():
ui.label("Login")
with ui.column():
login_input = ui.input("Username").classes("w-full")
password_input = ui.input("Password", password=True, password_toggle_button=True).classes("w-full")
ui.button("Login", on_click=try_create_user).classes("w-full")
await dialog
+227
View File
@@ -0,0 +1,227 @@
import asyncio
import logging
from functools import partial
from nicegui import ui
import config
import globals
from rooms.state import PlayerState
from web.custom_widgets.header import draw_header
from web.misc import check_user
def _check_room(room_uid: str):
return not globals.ROOMS_DATABASE.by_uid.get(room_uid) is None
async def _join_room(room_uid: str, user_uid: str):
for _ in range(10):
if globals.ROOMS_DATABASE.by_uid.get(room_uid) is None:
await asyncio.sleep(0.1)
continue
break
if globals.ROOMS_DATABASE.by_uid.get(room_uid) is None:
ui.notify(f"Failed to join room {room_uid}, redirecting to /rooms...", type="warning")
await asyncio.sleep(1)
ui.navigate.to("/rooms")
return
globals.ROOMS_DATABASE.by_uid[room_uid].connected_users.append(user_uid)
logging.info(f"{user_uid} joined room {room_uid}")
def _leave_room(room_uid: str, user_uid: str):
if user_uid in globals.ROOMS_DATABASE.by_uid[room_uid].connected_users:
globals.ROOMS_DATABASE.by_uid[room_uid].connected_users.remove(user_uid)
logging.info(f"{user_uid} left room {room_uid}")
def _change_episode(room_uid: str, tmdb_id: int, season_number: int, episode_number: int, seasons_column: ui.column,
video_player: ui.video, player_data: dict):
try:
new_episode = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id].seasons[season_number - 1].episodes[
episode_number - 1]
except KeyError:
ui.notify("Episode not found", type="negative")
return
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.current_season, room.current_episode = season_number, episode_number
room.player_position = 0
room.player_state = PlayerState.PAUSED
player_data["season"], player_data["episode"] = season_number, episode_number
player_data["position"] = 0
video_player.pause()
video_player.seek(0)
video_player.pause()
video_player.set_source(new_episode.file_url)
seasons_column.clear()
_draw_seasons(room_uid, tmdb_id, seasons_column, video_player, player_data)
def _draw_users_list(room_uid: str, users_card: ui.card):
users_card.clear()
room = globals.ROOMS_DATABASE.by_uid[room_uid]
for user_uid in room.connected_users:
user = globals.USERS_DATABASE.by_uid[user_uid]
with users_card, ui.card().classes("w-full"):
ui.label(user.username)
def _draw_seasons(room_uid: str, tmdb_id: int, seasons_column: ui.column, video_player: ui.video,
player_data: dict):
room = globals.ROOMS_DATABASE.by_uid[room_uid]
tv_show = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
for season_number in range(1, tv_show.number_of_seasons + 1):
season = tv_show.seasons[season_number - 1]
with seasons_column, ui.card().classes("w-full"):
ui.html(f"<b>{season.title}</b>")
with ui.row().classes("w-full"):
episodes = tv_show.seasons[season_number - 1].episodes
for episode_number in range(1, len(episodes) + 1):
episode_button = ui.button(str(episode_number),
on_click=partial(_change_episode, room_uid, tmdb_id, season_number,
episode_number, seasons_column, video_player,
player_data))
if room.current_season == season_number and room.current_episode == episode_number:
episode_button.disable()
async def _on_seeked(room_uid: str, video_player: ui.video, player_data: dict):
try:
position = await ui.run_javascript(f'getHtmlElement({video_player.id}).currentTime')
except TimeoutError:
return
globals.ROOMS_DATABASE.by_uid[room_uid].seek(position)
player_data["position"] = position
async def _on_play(room_uid: str, video_player: ui.video, player_data: dict):
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.play()
player_data["state"] = PlayerState.PLAYING
await _on_seeked(room_uid, video_player, player_data)
async def _on_pause(room_uid: str, video_player: ui.video, player_data: dict):
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.pause()
player_data["state"] = PlayerState.PAUSED
await _on_seeked(room_uid, video_player, player_data)
async def _on_stop(room_uid: str, video_player: ui.video, player_data: dict):
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.stop()
player_data["state"] = PlayerState.STOPPED
await _on_seeked(room_uid, video_player, player_data)
video_player.pause()
async def _sync(room_uid: str, tmdb_id: int, seasons_column: ui.column, video_player: ui.video,
player_data: dict):
try:
player_data["position"] = await ui.run_javascript(f'getHtmlElement({video_player.id}).currentTime')
except TimeoutError:
return
room = globals.ROOMS_DATABASE.by_uid[room_uid]
if player_data["state"] != room.player_state:
match room.player_state:
case PlayerState.PLAYING:
video_player.play()
player_data["state"] = PlayerState.PLAYING
case PlayerState.PAUSED:
video_player.pause()
player_data["state"] = PlayerState.PAUSED
case PlayerState.STOPPED:
video_player.pause()
player_data["state"] = PlayerState.STOPPED
if abs(player_data["position"] - room.player_position) > config.MAX_DELAY_SECONDS:
video_player.seek(room.player_position)
player_data["position"] = room.player_position
if player_data["season"] != room.current_season or player_data["episode"] != room.current_episode:
player_data["season"], player_data["episode"] = room.current_season, room.current_episode
_change_episode(room_uid, tmdb_id, room.current_season, room.current_episode, seasons_column, video_player,
player_data)
async def page(room_uid: str):
if not _check_room(room_uid):
ui.navigate.to("/rooms")
return
user = await check_user()
if not user:
ui.navigate.to("/")
return
await draw_header()
await _join_room(room_uid, user.uid)
player_data = {
"state": PlayerState.PAUSED,
"position": 0,
"season": None,
"episode": None
}
with ui.row(wrap=False).classes("w-full"):
player_card = ui.card()
player_card.classes("no-shadow")
player_card.style("width: 80%; height: 85vh; border-radius: 15px")
player_card.style("display: flex; justify-content: center; align-items: center;")
users_card = ui.card()
users_card.classes("no-shadow")
users_card.style("width: 20%; height: 85vh; border-radius: 15px")
ui.timer(1, partial(_draw_users_list, room_uid, users_card))
with player_card:
video_player = ui.video(src="")
video_player.classes("w-full h-full")
video_player.style("border-radius: 15px")
video_player.on("play", partial(_on_play, room_uid, video_player, player_data))
video_player.on("pause", partial(_on_pause, room_uid, video_player, player_data))
video_player.on("seeked", partial(_on_seeked, room_uid, video_player, player_data))
video_player.on("ended", partial(_on_stop, room_uid, video_player, player_data))
seasons_column = ui.column().classes("w-full")
seasons_column.visible = False
tmdb_id = globals.ROOMS_DATABASE.by_uid[room_uid].tmdb_id
content = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
if content.type == "movie":
source = content.file_url
elif content.type == "tv":
source = content.seasons[0].episodes[0].file_url
room = globals.ROOMS_DATABASE.by_uid[room_uid]
room.current_season, room.current_episode = 1, 1
player_data["season"], player_data["episode"] = 1, 1
seasons_column.visible = True
_draw_seasons(room_uid, tmdb_id, seasons_column, video_player, player_data)
else:
source = ""
video_player.set_source(source)
ui.timer(1, partial(_sync, room_uid, tmdb_id, seasons_column, video_player, player_data))
await ui.context.client.disconnected()
_leave_room(room_uid, user.uid)