Skip to content

Commit

Permalink
feat: improved ui (#422)
Browse files Browse the repository at this point in the history
* testing

* testing

* minor fixes

* feat: some frontend ui changes :)

* minor tweaks

* added movies and top movies section

* nearly completed homepage

* fix: fix settings and improvements to homepage

* chore: update app name to Riven and fix page header in library and onboarding

* feat: switch to vaul-svelte dependency for mobile ui and improvements to ui in general

* chore: update app name to Riven, improve UI consistency and changes to items endpoint

* feat: Add incomplete items to statistics page

* feat: Add services status to statistics page
  • Loading branch information
AyushSehrawat authored Jul 9, 2024
1 parent 0dbc9f7 commit 71e6365
Show file tree
Hide file tree
Showing 41 changed files with 4,142 additions and 2,333 deletions.
179 changes: 103 additions & 76 deletions backend/controllers/items.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import datetime
from enum import Enum
from typing import List, Optional

import Levenshtein
Expand Down Expand Up @@ -30,41 +31,30 @@ async def get_states():
}


@router.get("/", summary="Retrieve Media Items", description="Fetch media items with optional filters and pagination.")
@router.get(
"",
summary="Retrieve Media Items",
description="Fetch media items with optional filters and pagination",
)
async def get_items(
request: Request,
fetch_all: Optional[bool] = False,
limit: Optional[int] = 20,
limit: Optional[int] = 50,
page: Optional[int] = 1,
search: Optional[str] = None,
type: Optional[str] = None,
state: Optional[str] = None,
type: Optional[str] = None
sort: Optional[str] = "desc",
search: Optional[str] = None,
):
"""
Fetch media items with optional filters and pagination.
Parameters:
- request: Request object
- fetch_all: Fetch all items without pagination (default: False)
- limit: Number of items per page (default: 20)
- page: Page number (default: 1)
- search: Search term to filter items by title, IMDb ID, or item ID
- state: Filter items by state
- type: Filter items by type (movie, show, season, episode)
Returns:
- JSON response with success status, items, pagination details, and total count
Examples:
- Fetch all items: /items?fetch_all=true
- Fetch first 10 items: /items?limit=10&page=1
- Search items by title: /items?search=inception
- Filter items by state: /items?state=completed
- Filter items by type: /items?type=movie
"""
if page < 1:
raise HTTPException(status_code=400, detail="Page number must be 1 or greater.")

if limit < 1:
raise HTTPException(status_code=400, detail="Limit must be 1 or greater.")

items = list(request.app.program.media_items._items.values())
total_items = len(items)

if search:
if search: # TODO: fix for search
search_lower = search.lower()
filtered_items = []
if search_lower.startswith("tt"):
Expand All @@ -76,25 +66,20 @@ async def get_items(
else:
for item in items:
if isinstance(item, MediaItem):
title_match = item.title and Levenshtein.distance(search_lower, item.title.lower()) <= 0.90
imdb_match = item.imdb_id and Levenshtein.distance(search_lower, item.imdb_id.lower()) <= 1
title_match = (
item.title
and Levenshtein.distance(search_lower, item.title.lower())
<= 0.90
)
imdb_match = (
item.imdb_id
and Levenshtein.distance(search_lower, item.imdb_id.lower())
<= 1
)
if title_match or imdb_match:
filtered_items.append(item)
items = filtered_items

if type:
type_lower = type.lower()
if type_lower == "movie":
items = list(request.app.program.media_items.movies.values())
elif type_lower == "show":
items = list(request.app.program.media_items.shows.values())
elif type_lower == "season":
items = list(request.app.program.media_items.seasons.values())
elif type_lower == "episode":
items = list(request.app.program.media_items.episodes.values())
else:
raise HTTPException(status_code=400, detail=f"Invalid type: {type}. Valid types are: ['movie', 'show', 'season', 'episode']")

if state:
filter_lower = state.lower()
filter_state = None
Expand All @@ -106,28 +91,56 @@ async def get_items(
items = [item for item in items if item.state == filter_state]
else:
valid_states = [state.name for state in States]
raise HTTPException(status_code=400, detail=f"Invalid filter state: {state}. Valid states are: {valid_states}")
raise HTTPException(
status_code=400,
detail=f"Invalid filter state: {state}. Valid states are: {valid_states}",
)

if not fetch_all:
if page < 1:
raise HTTPException(status_code=400, detail="Page number must be 1 or greater.")
if limit < 1:
raise HTTPException(status_code=400, detail="Limit must be 1 or greater.")

start = (page - 1) * limit
end = start + limit
items = items[start:end]
if type:
type_lower = type.lower()
if type_lower == "movie":
items = list(request.app.program.media_items.movies.values())
total_items = len(items)
elif type_lower == "show":
items = list(request.app.program.media_items.shows.values())
total_items = len(items)
elif type_lower == "season":
items = list(request.app.program.media_items.seasons.values())
total_items = len(items)
elif type_lower == "episode":
items = list(request.app.program.media_items.episodes.values())
total_items = len(items)
else:
raise HTTPException(
status_code=400,
detail=f"Invalid type: {type}. Valid types are: ['movie', 'show', 'season', 'episode']",
)

if (
sort and not search
): # we don't want to sort search results as they are already sorted by relevance
if sort.lower() == "asc":
items = sorted(items, key=lambda x: x.requested_at)
elif sort.lower() == "desc":
items = sorted(items, key=lambda x: x.requested_at, reverse=True)
else:
raise HTTPException(
status_code=400,
detail=f"Invalid sort: {sort}. Valid sorts are: ['asc', 'desc']",
)

total_count = len(items)
total_pages = (total_count + limit - 1) // limit
start = (page - 1) * limit
end = start + limit
items = items[start:end]
total_pages = (total_items + limit - 1) // limit

return {
"success": True,
"items": [item.to_dict() for item in items],
"page": page,
"limit": limit,
"total": total_count,
"total_pages": total_pages
"total_items": total_items,
"total_pages": total_pages,
}


Expand All @@ -145,10 +158,14 @@ async def get_extended_item_info(request: Request, item_id: str):

@router.post("/add/imdb/{imdb_id}")
@router.post("/add/imdb/")
async def add_items(request: Request, imdb_id: Optional[str] = None, imdb_ids: Optional[IMDbIDs] = None):
async def add_items(
request: Request, imdb_id: Optional[str] = None, imdb_ids: Optional[IMDbIDs] = None
):
if imdb_id:
imdb_ids = IMDbIDs(imdb_ids=[imdb_id])
elif not imdb_ids or not imdb_ids.imdb_ids or any(not id for id in imdb_ids.imdb_ids):
elif (
not imdb_ids or not imdb_ids.imdb_ids or any(not id for id in imdb_ids.imdb_ids)
):
raise HTTPException(status_code=400, detail="No IMDb ID(s) provided")

valid_ids = []
Expand All @@ -164,21 +181,21 @@ async def add_items(request: Request, imdb_id: Optional[str] = None, imdb_ids: O
for id in valid_ids:
item = MediaItem({"imdb_id": id, "requested_by": "riven"})
request.app.program.add_to_queue(item)

return {"success": True, "message": f"Added {len(valid_ids)} item(s) to the queue"}


@router.delete("/remove/")
async def remove_item(
request: Request,
item_id: Optional[str] = None,
imdb_id: Optional[str] = None
request: Request, item_id: Optional[str] = None, imdb_id: Optional[str] = None
):
if item_id:
item = request.app.program.media_items.get(ItemId(item_id))
id_type = "ID"
elif imdb_id:
item = next((i for i in request.app.program.media_items if i.imdb_id == imdb_id), None)
item = next(
(i for i in request.app.program.media_items if i.imdb_id == imdb_id), None
)
id_type = "IMDb ID"
else:
raise HTTPException(status_code=400, detail="No item ID or IMDb ID provided")
Expand All @@ -187,7 +204,7 @@ async def remove_item(
logger.error(f"Item with {id_type} {item_id or imdb_id} not found")
return {
"success": False,
"message": f"Item with {id_type} {item_id or imdb_id} not found. No action taken."
"message": f"Item with {id_type} {item_id or imdb_id} not found. No action taken.",
}

try:
Expand All @@ -198,22 +215,33 @@ async def remove_item(
# Remove the symlinks associated with the item
symlinker = request.app.program.service[Symlinker]
symlinker.delete_item_symlinks(item)
logger.log("API", f"Removed symlink for item with {id_type} {item_id or imdb_id}")
logger.log(
"API", f"Removed symlink for item with {id_type} {item_id or imdb_id}"
)

# Save and reload the media items to ensure consistency
symlinker.save_and_reload_media_items(request.app.program.media_items)
logger.log("API", f"Saved and reloaded media items after removing item with {id_type} {item_id or imdb_id}")
logger.log(
"API",
f"Saved and reloaded media items after removing item with {id_type} {item_id or imdb_id}",
)

return {
"success": True,
"message": f"Successfully removed item with {id_type} {item_id or imdb_id}."
"message": f"Successfully removed item with {id_type} {item_id or imdb_id}.",
}
except Exception as e:
logger.error(f"Failed to remove item with {id_type} {item_id or imdb_id}: {e}")
raise HTTPException(status_code=500, detail="Internal server error")


@router.get("/imdb/{imdb_id}")
async def get_imdb_info(request: Request, imdb_id: str, season: Optional[int] = None, episode: Optional[int] = None):
async def get_imdb_info(
request: Request,
imdb_id: str,
season: Optional[int] = None,
episode: Optional[int] = None,
):
"""
Get the item with the given IMDb ID.
If the season and episode are provided, get the item with the given season and episode.
Expand All @@ -223,7 +251,7 @@ async def get_imdb_info(request: Request, imdb_id: str, season: Optional[int] =
item_id = ItemId(str(season), parent_id=item_id)
if episode is not None:
item_id = ItemId(str(episode), parent_id=item_id)

item = request.app.program.media_items.get_item(item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
Expand All @@ -233,18 +261,17 @@ async def get_imdb_info(request: Request, imdb_id: str, season: Optional[int] =

@router.get("/incomplete")
async def get_incomplete_items(request: Request):
if not hasattr(request.app, 'program') or not hasattr(request.app.program, 'media_items'):
if not hasattr(request.app, "program") or not hasattr(
request.app.program, "media_items"
):
logger.error("Program or media_items not found in the request app")
raise HTTPException(status_code=500, detail="Internal server error")

incomplete_items = request.app.program.media_items.get_incomplete_items()
if not incomplete_items:
return {
"success": True,
"incomplete_items": []
}
return {"success": True, "incomplete_items": []}

return {
"success": True,
"incomplete_items": [item.to_dict() for item in incomplete_items.values()]
"incomplete_items": [item.to_dict() for item in incomplete_items.values()],
}
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"bits-ui": "^0.21.10",
"clsx": "^2.1.1",
"cmdk-sv": "^0.0.17",
"embla-carousel-autoplay": "^8.1.5",
"embla-carousel-svelte": "^8.1.5",
"formsnap": "^1.0.0",
"lucide-svelte": "^0.390.0",
Expand All @@ -56,6 +57,7 @@
"tailwind-merge": "^2.3.0",
"tailwind-variants": "^0.2.1",
"uuid": "^9.0.1",
"vaul-svelte": "^0.3.2",
"zod": "^3.23.8"
}
}
Loading

0 comments on commit 71e6365

Please sign in to comment.