initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from web.web import *
|
||||
@@ -0,0 +1,3 @@
|
||||
from web.custom_widgets.content_card import ContentCard
|
||||
from web.custom_widgets.content_dialog import ContentDialog
|
||||
from web.custom_widgets.header import draw_header
|
||||
@@ -0,0 +1,48 @@
|
||||
from nicegui import ui
|
||||
|
||||
import globals
|
||||
from web.custom_widgets import ContentDialog
|
||||
|
||||
|
||||
class ContentCard:
|
||||
def __init__(self, tmdb_id: int):
|
||||
self.content = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
|
||||
|
||||
self.dialog = ContentDialog(tmdb_id)
|
||||
|
||||
self.card = ui.card()
|
||||
self.card.classes("no-shadow")
|
||||
self.card.style("margin: 0; padding: 6px; border-radius: 20px")
|
||||
|
||||
with self.card:
|
||||
self.poster_image = ui.image(source=self.content.poster_url)
|
||||
self.poster_image.style("width: 200px; height: 100%; border-radius: 15px")
|
||||
|
||||
with self.poster_image:
|
||||
self.poster_column = ui.column()
|
||||
self.poster_column.classes("w-full h-full justify-between")
|
||||
self.poster_column.style(
|
||||
"margin: 0; padding:0; opacity: 0; transition-duration: 0.5s")
|
||||
|
||||
with self.poster_column:
|
||||
with ui.column(wrap=False).classes("w-full").style("gap: 0px; margin: 0; padding: 3px 3px 3px 10px;"):
|
||||
ui.html(f"<b>{self.content.title}</b>").style("font-size: 16px")
|
||||
ui.html(f"<i>{self.content.og_title}</i>").style("font-size: 10px")
|
||||
|
||||
with ui.column(wrap=True).classes("w-full").style("gap: 0px; margin: 0; padding: 3px 3px 10px 10px"):
|
||||
ui.html(f"<b><i>{"<br>".join((self.content.genres))}</i></b>").style("font-size: 8px")
|
||||
|
||||
self.card.on("mouseover", self.on_card_hover)
|
||||
self.card.on("mouseleave", self.on_card_unhover)
|
||||
self.card.on("click", self.on_card_click)
|
||||
|
||||
def on_card_hover(self):
|
||||
self.poster_column.style("opacity: 0.9")
|
||||
self.card.style("transform: scale(1.02)")
|
||||
|
||||
def on_card_unhover(self):
|
||||
self.poster_column.style("opacity: 0")
|
||||
self.card.style("transform: scale(1.0)")
|
||||
|
||||
def on_card_click(self):
|
||||
self.dialog.open()
|
||||
@@ -0,0 +1,66 @@
|
||||
import itertools
|
||||
|
||||
from nicegui import ui
|
||||
|
||||
import globals
|
||||
from web.misc import convert_runtime
|
||||
|
||||
|
||||
class ContentDialog(ui.dialog):
|
||||
def __init__(self, tmdb_id: int, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.content = globals.MOVIES_DATABASE.by_tmdb_id[tmdb_id]
|
||||
|
||||
with self:
|
||||
self.card = ui.card()
|
||||
self.card.classes("h-max no-shadow")
|
||||
self.card.style("min-width: 40%; min-height: 60%; border-radius: 15px")
|
||||
|
||||
with self.card:
|
||||
self.title_column = ui.column(wrap=False)
|
||||
self.title_column.classes("w-full")
|
||||
self.title_column.style("gap: 0; margin: 0; padding: 0;")
|
||||
|
||||
with self.title_column:
|
||||
ui.html(f"<b>{self.content.title}</b>").style("font-size: 22px")
|
||||
ui.html(f"<i>{self.content.og_title}</i>").style("font-size: 14px")
|
||||
ui.html(f"<b><i>{", ".join(self.content.genres)}</i></b>").style("font-size: 12px")
|
||||
|
||||
with self.card:
|
||||
self.main_row = ui.row(wrap=False)
|
||||
self.main_row.classes("w-full h-full")
|
||||
|
||||
with self.main_row:
|
||||
ui.image(source=self.content.poster_url).style("width: 50%; border-radius: 15px")
|
||||
|
||||
self.additional_info_column = ui.column(wrap=False)
|
||||
self.additional_info_column.style("width: 50%")
|
||||
|
||||
with self.additional_info_column:
|
||||
release_date = self.content.release_date.split("-")[::-1]
|
||||
release_date = ".".join(release_date)
|
||||
ui.html(f"<b>Release Date: </b>{release_date}").style("font-size: 12px")
|
||||
ui.html(f"<b>Average Score: </b>{round(self.content.vote_average, 2)}").style("font-size: 12px")
|
||||
|
||||
if self.content.type == "movie":
|
||||
budget = "$" + f"{self.content.budget:_}".replace("_", ".")
|
||||
ui.html(f"<b>Budget: </b>{budget}").style("font-size: 12px")
|
||||
ui.html(f"<b>Runtime: </b>{convert_runtime(self.content.runtime)}").style("font-size: 12px")
|
||||
elif self.content.type == "tv":
|
||||
ui.html(f"<b>Number of Seasons: </b>{self.content.number_of_seasons}").style("font-size: 12px")
|
||||
ui.html(f"<b>Number of Episodes: </b>{self.content.number_of_episodes}").style("font-size: 12px")
|
||||
total_runtime = sum([ep.runtime for ep in
|
||||
itertools.chain.from_iterable([s.episodes for s in self.content.seasons])])
|
||||
ui.html(f"<b>Total Runtime: </b>{convert_runtime(total_runtime)}").style("font-size: 12px")
|
||||
if self.content.in_production:
|
||||
ui.html(f"<b><i>Currently in production</i></b>").style("font-size: 12px")
|
||||
else:
|
||||
ui.html(f"<b><i>Finished</i></b>").style("font-size: 12px")
|
||||
|
||||
with self.card, ui.row(wrap=False).classes("w-full").style("display: flex; justify-content: center;"):
|
||||
ui.button("Create Room").on_click(self.create_room)
|
||||
|
||||
async def create_room(self):
|
||||
room = await globals.ROOMS_DATABASE.create_room(self.content.tmdb_id)
|
||||
ui.navigate.to(f"/room/{room.uid}")
|
||||
@@ -0,0 +1,11 @@
|
||||
from nicegui import ui
|
||||
|
||||
from web.misc import logout
|
||||
|
||||
|
||||
async def draw_header():
|
||||
with ui.header(wrap=False):
|
||||
with ui.row(wrap=False).classes("w-full justify-between") as header_row:
|
||||
ui.button(icon="home", on_click=lambda: ui.navigate.to("/"))
|
||||
ui.button(text="Rooms", icon="movie", on_click=lambda: ui.navigate.to("/rooms"))
|
||||
ui.button(icon="logout", on_click=lambda: logout(redirect=True))
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
from nicegui import ui, app
|
||||
|
||||
import globals
|
||||
from users.classes import User
|
||||
|
||||
|
||||
def convert_runtime(total_minutes: int) -> str:
|
||||
hours = total_minutes // 60
|
||||
minutes = total_minutes % 60
|
||||
|
||||
if hours > 0 and minutes > 0:
|
||||
return f"{hours} h {minutes} m"
|
||||
elif hours > 0:
|
||||
return f"{hours} h"
|
||||
elif minutes > 0:
|
||||
return f"{minutes} m"
|
||||
else:
|
||||
return "0 m"
|
||||
|
||||
|
||||
def logout(redirect: bool = False):
|
||||
if app.storage.user.get("token"):
|
||||
app.storage.user.pop("token")
|
||||
|
||||
if redirect:
|
||||
ui.navigate.to("/")
|
||||
|
||||
|
||||
async def check_user() -> User | None:
|
||||
if token := app.storage.user.get("token"):
|
||||
user = await globals.USERS_DATABASE.get_user_by_token(token)
|
||||
if user:
|
||||
return user
|
||||
else:
|
||||
logout()
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
async def update_user():
|
||||
if token := app.storage.user.get("token"):
|
||||
user = await globals.USERS_DATABASE.get_user_by_token(token)
|
||||
if user:
|
||||
await globals.USERS_DATABASE.update_user(user.uid)
|
||||
else:
|
||||
app.storage.user.pop("token")
|
||||
ui.navigate.to("/")
|
||||
else:
|
||||
ui.navigate.to("/")
|
||||
@@ -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
|
||||
@@ -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>")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
from nicegui import ui
|
||||
|
||||
import config
|
||||
from web.misc import update_user
|
||||
from web.pages import *
|
||||
|
||||
|
||||
@ui.page("/")
|
||||
async def index():
|
||||
ui.add_head_html("<meta name=\"referrer\" content=\"no-referrer\" />")
|
||||
await ui.context.client.connected(timeout=config.CONNECTION_TIMEOUT_SECONDS)
|
||||
await index_page.page()
|
||||
|
||||
|
||||
@ui.page("/contents")
|
||||
async def movies():
|
||||
ui.add_head_html("<meta name=\"referrer\" content=\"no-referrer\" />")
|
||||
await ui.context.client.connected(timeout=config.CONNECTION_TIMEOUT_SECONDS)
|
||||
ui.timer(60, update_user)
|
||||
await movies_page.page()
|
||||
|
||||
|
||||
@ui.page("/rooms")
|
||||
async def rooms():
|
||||
ui.add_head_html("<meta name=\"referrer\" content=\"no-referrer\" />")
|
||||
await ui.context.client.connected(timeout=config.CONNECTION_TIMEOUT_SECONDS)
|
||||
ui.timer(60, update_user)
|
||||
await rooms_page.page()
|
||||
|
||||
|
||||
@ui.page("/room/{room_uid}")
|
||||
async def room(room_uid: str):
|
||||
ui.add_head_html("<meta name=\"referrer\" content=\"no-referrer\" />")
|
||||
ui.add_head_html('''
|
||||
<link href="https://vjs.zencdn.net/7.21.1/video-js.css" rel="stylesheet" />
|
||||
<script src="https://vjs.zencdn.net/7.21.1/video.min.js"></script>
|
||||
''')
|
||||
await ui.context.client.connected(timeout=config.CONNECTION_TIMEOUT_SECONDS)
|
||||
ui.timer(60, update_user)
|
||||
await room_page.page(room_uid)
|
||||
Reference in New Issue
Block a user