Skip to content

Commit

Permalink
Fixes (#378)
Browse files Browse the repository at this point in the history
* chore: move to org

* fix: lots of fixes and prepwork

* feat: add bun lockfile to gitignore

* fix: work on plex updating for anime

---------

Co-authored-by: Spoked <Spoked@localhost>
  • Loading branch information
dreulavelle and Spoked authored Jun 11, 2024
1 parent f9b2feb commit 075f918
Show file tree
Hide file tree
Showing 22 changed files with 883 additions and 541 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ makefile
profile.svg
*.gz
*.zip
*.lockb

# Python bytecode / Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
211 changes: 143 additions & 68 deletions backend/controllers/items.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
from datetime import datetime
from typing import List, Optional

import Levenshtein
from fastapi import APIRouter, HTTPException, Request
from pydantic import BaseModel
from program.media.item import MediaItem
from program.content.overseerr import Overseerr
from program.media.container import MediaItemContainer
from program.media.item import ItemId, MediaItem
from program.media.state import States
from pydantic import BaseModel
from utils.logger import logger

router = APIRouter(
Expand All @@ -26,17 +29,104 @@ async def get_states():
}


@router.get("/")
async def get_items(request: Request):
@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,
page: Optional[int] = 1,
search: Optional[str] = None,
state: Optional[str] = None,
type: 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 or IMDb 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
"""
items = list(request.app.program.media_items._items.values())

if search:
search_lower = search.lower()
filtered_items = []
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
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
for state in States:
if Levenshtein.distance(filter_lower, state.name.lower()) <= 0.82:
filter_state = state
break
if filter_state:
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}")

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]

total_count = len(items)
total_pages = (total_count + limit - 1) // limit

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


@router.get("/extended/{item_id}")
async def get_extended_item_info(request: Request, item_id: str):
item = request.app.program.media_items.get_item(item_id)
mic: MediaItemContainer = request.app.program.media_items
item = mic.get(item_id)
if item is None:
raise HTTPException(status_code=404, detail="Item not found")
return {
Expand Down Expand Up @@ -70,77 +160,63 @@ async def add_items(request: Request, imdb_id: Optional[str] = None, imdb_ids: O
return {"success": True, "message": f"Added {len(valid_ids)} item(s) to the queue"}


@router.delete("/remove/id/{item_id}")
async def remove_item(request: Request, item_id: str):
item = request.app.program.media_items.get_item(item_id)
if not item:
logger.error(f"Item with ID {item_id} not found")
raise HTTPException(status_code=404, detail="Item not found")

request.app.program.media_items.remove(item)
if item.symlinked:
request.app.program.media_items.remove_symlink(item)
logger.log("API", f"Removed symlink for item with ID {item_id}")

overseerr_service = request.app.program.services.get(Overseerr)
if overseerr_service and overseerr_service.initialized:
try:
overseerr_result = overseerr_service.delete_request(item_id)
if overseerr_result:
logger.log("API", f"Deleted Overseerr request for item with ID {item_id}")
else:
logger.log("API", f"Failed to delete Overseerr request for item with ID {item_id}")
except Exception as e:
logger.error(f"Exception occurred while deleting Overseerr request for item with ID {item_id}: {e}")

return {
"success": True,
"message": f"Removed {item_id}",
}

@router.delete("/remove/")
async def remove_item(
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)
id_type = "IMDb ID"
else:
raise HTTPException(status_code=400, detail="No item ID or IMDb ID provided")

@router.delete("/remove/imdb/{imdb_id}")
async def remove_item_by_imdb(request: Request, imdb_id: str):
item = request.app.program.media_items.get_item(imdb_id)
if not item:
logger.error(f"Item with IMDb ID {imdb_id} not found")
raise HTTPException(status_code=404, detail="Item not found")
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."
}

request.app.program.media_items.remove(item)
if item.symlinked or (item.file and item.folder): # TODO: this needs to be checked later..
request.app.program.media_items.remove_symlink(item)
logger.log("API", f"Removed symlink for item with IMDb ID {imdb_id}")

overseerr_service = request.app.program.services.get(Overseerr)
if overseerr_service and overseerr_service.initialized:
try:
overseerr_result = overseerr_service.delete_request(item.overseerr_id)
if overseerr_result:
logger.log("API", f"Deleted Overseerr request for item with IMDb ID {imdb_id}")
else:
logger.error(f"Failed to delete Overseerr request for item with IMDb ID {imdb_id}")
except Exception as e:
logger.error(f"Exception occurred while deleting Overseerr request for item with IMDb ID {imdb_id}: {e}")
else:
logger.error("Overseerr service not found in program services")
try:
# Remove the item from the media items container
request.app.program.media_items.remove(item)
logger.log("API", f"Removed item with {id_type} {item_id or imdb_id}")

return {
"success": True,
"message": f"Removed item with IMDb ID {imdb_id}",
}
# Remove the symlinks associated with the item
symlinker = request.app.program.symlinker
symlinker.delete_item_symlinks(item)
logger.log("API", f"Removed symlink for 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}."
}
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):
item = request.app.program.media_items.get_item(imdb_id)
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.
"""
item_id = ItemId(imdb_id)
if season is not None:
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:
logger.error(f"Item with IMDb ID {imdb_id} not found in container")
raise HTTPException(status_code=404, detail="Item not found")

if not request.app.program.media_items.__contains__(item):
logger.error(f"Item with IMDb ID {imdb_id} is not in the library")
raise HTTPException(status_code=404, detail="Item not found in library")

return {"success": True, "item": item.to_extended_dict()}


Expand All @@ -152,7 +228,6 @@ async def get_incomplete_items(request: Request):

incomplete_items = request.app.program.media_items.get_incomplete_items()
if not incomplete_items:
logger.info("No incomplete items found")
return {
"success": True,
"incomplete_items": []
Expand Down
17 changes: 13 additions & 4 deletions backend/controllers/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@

import pydantic
from fastapi import APIRouter, Request
from program.media.item import MediaItem
from program.content.overseerr import Overseerr
from program.indexers.trakt import get_imdbid_from_tmdb
from utils.request import get
from program.indexers.trakt import (
TraktIndexer,
create_item_from_imdb_id,
get_imdbid_from_tmdb,
)
from program.media.item import MediaItem, Show
from utils.logger import logger
from utils.request import get

from .models.overseerr import OverseerrWebhook

Expand Down Expand Up @@ -43,12 +47,17 @@ async def overseerr(request: Request) -> Dict[str, Any]:
return {"success": False, "message": "Failed to get imdb_id from TMDB", "title": req.subject}

overseerr: Overseerr = request.app.program.services[Overseerr]
trakt: TraktIndexer = request.app.program.services[TraktIndexer]

if imdb_id in overseerr.recurring_items:
logger.log("API", "Request already in queue", {"imdb_id": imdb_id})
return {"success": False, "message": "Request already in queue", "title": req.subject}
else:
overseerr.recurring_items.add(imdb_id)

item = MediaItem({"imdb_id": imdb_id, "requested_by": "overseerr", "requested_at": datetime.now()})
new_item = MediaItem({"imdb_id": imdb_id, "requested_by": "overseerr"})
item = create_item_from_imdb_id(new_item.imdb_id)
if isinstance(item, Show):
trakt._add_seasons_to_show(item, imdb_id)
request.app.program.add_to_queue(item)
return {"success": True}
Loading

0 comments on commit 075f918

Please sign in to comment.