Skip to content

Commit

Permalink
Fix (#395)
Browse files Browse the repository at this point in the history
* fix: set uid/gid correctly

* feat: add show packs

* feat: large update

* fix: path/str on save

* fix: tidy up

* Very small improvements to logs

* Fix issue when max_size is -1

* feat: new prog feature + debrid fix

* fix: testing. wip

* Add trakt collection as content source

* fix: further testing

* fix: merge changes from 0x

* feat: add logging for individual items

* fix: return unknown state as default

* fix: files missing in dict

* feat: add max worker count. other tweaks

* feat: add final touches

* fix: change exception to error in plex updater

* fix: add shallow copy on retry_library

---------

Co-authored-by: Spoked <Spoked@localhost>
Co-authored-by: davidemarcoli <[email protected]>
  • Loading branch information
3 people authored Jun 16, 2024
1 parent 282eb35 commit 8e3c63b
Show file tree
Hide file tree
Showing 43 changed files with 1,248 additions and 684 deletions.
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLAYE/---bug-report.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: "\U0001F41E Bug Report"
description: "Iceberg not working the way it is documented?"
description: "Riven not working the way it is documented?"
title: "[Bug]: "
labels: ["kind/bug", "status/triage"]
assignees:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ profile.svg
*.zip
*.lockb
*.pkl
*.bak

# Python bytecode / Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
38 changes: 19 additions & 19 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ RUN pnpm run build && pnpm prune --prod

# Final Image
FROM python:3.11-alpine
LABEL name="Iceberg" \
description="Iceberg Debrid Downloader" \
url="https://github.com/dreulavelle/iceberg"
LABEL name="Riven" \
description="Riven Media Server" \
url="https://github.com/rivenmedia/riven"

# Install system dependencies and Node.js
ENV PYTHONUNBUFFERED=1
Expand Down Expand Up @@ -67,41 +67,41 @@ RUN mkdir -p /usr/share/fonts/nerd-fonts && \
RUN pip install poetry==1.8.3

# Create user and group
RUN addgroup -g 1000 iceberg && \
adduser -u 1000 -G iceberg -h /home/iceberg -s /usr/bin/fish -D iceberg
RUN addgroup -g 1000 riven && \
adduser -u 1000 -G riven -h /home/riven -s /usr/bin/fish -D riven

# Create fish config directory
RUN mkdir -p /home/iceberg/.config/fish
RUN mkdir -p /home/riven/.config/fish

# Set environment variable to force color output
ENV FORCE_COLOR=1
ENV TERM=xterm-256color

# Set working directory
WORKDIR /iceberg
WORKDIR /riven

# Copy frontend build from the previous stage
COPY --from=frontend --chown=riven:riven /app/build /riven/frontend/build
COPY --from=frontend --chown=riven:riven /app/node_modules /riven/frontend/node_modules
COPY --from=frontend --chown=riven:riven /app/package.json /riven/frontend/package.json

# Copy the virtual environment from the builder stage
COPY --from=builder /app/.venv /app/.venv
ENV VIRTUAL_ENV=/app/.venv
ENV PATH="/app/.venv/bin:$PATH"

# Copy the rest of the application code
COPY backend/ /iceberg/backend
COPY pyproject.toml poetry.lock /iceberg/backend/
COPY VERSION entrypoint.sh /iceberg/

# Copy frontend build from the previous stage
COPY --from=frontend --chown=iceberg:iceberg /app/build /iceberg/frontend/build
COPY --from=frontend --chown=iceberg:iceberg /app/node_modules /iceberg/frontend/node_modules
COPY --from=frontend --chown=iceberg:iceberg /app/package.json /iceberg/frontend/package.json
COPY backend/ /riven/backend
COPY pyproject.toml poetry.lock /riven/backend/
COPY VERSION entrypoint.sh /riven/

# Ensure entrypoint script is executable
RUN chmod +x /iceberg/entrypoint.sh
RUN chmod +x /riven/entrypoint.sh

# Set correct permissions for the iceberg user
RUN chown -R iceberg:iceberg /home/iceberg/.config /iceberg
# Set correct permissions for the riven user
RUN chown -R riven:riven /home/riven/.config /riven

# Switch to fish shell
SHELL ["fish", "--login"]

ENTRYPOINT ["fish", "/iceberg/entrypoint.sh"]
ENTRYPOINT ["fish", "/riven/entrypoint.sh"]
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.5
0.7.0
35 changes: 23 additions & 12 deletions backend/controllers/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from fastapi import APIRouter, HTTPException, Request
from program.content.overseerr import Overseerr
from program.media.container import MediaItemContainer
from program.media.item import ItemId, MediaItem
from program.media.item import Episode, ItemId, MediaItem, Movie, Season, Show
from program.media.state import States
from program.symlink import Symlinker
from pydantic import BaseModel
from utils.logger import logger

Expand Down Expand Up @@ -47,7 +48,7 @@ async def get_items(
- 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
- 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)
Expand All @@ -66,12 +67,19 @@ async def get_items(
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)
if search_lower.startswith("tt"):
item = request.app.program.media_items.get_item(ItemId(search_lower))
if item:
filtered_items.append(item)
else:
raise HTTPException(status_code=404, detail="Item not found.")
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
if title_match or imdb_match:
filtered_items.append(item)
items = filtered_items

if type:
Expand Down Expand Up @@ -154,7 +162,7 @@ async def add_items(request: Request, imdb_id: Optional[str] = None, imdb_ids: O
raise HTTPException(status_code=400, detail="No valid IMDb ID(s) provided")

for id in valid_ids:
item = MediaItem({"imdb_id": id, "requested_by": "iceberg", "requested_at": datetime.now()})
item = MediaItem({"imdb_id": id, "requested_by": "riven", "requested_at": datetime.now()})
request.app.program.add_to_queue(item)

return {"success": True, "message": f"Added {len(valid_ids)} item(s) to the queue"}
Expand Down Expand Up @@ -184,14 +192,18 @@ async def remove_item(

try:
# Remove the item from the media items container
request.app.program.media_items.remove(item)
request.app.program.media_items.remove([item])
logger.log("API", f"Removed item with {id_type} {item_id or imdb_id}")

# Remove the symlinks associated with the item
symlinker = request.app.program.symlinker
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}")

# 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}")

return {
"success": True,
"message": f"Successfully removed item with {id_type} {item_id or imdb_id}."
Expand All @@ -200,7 +212,6 @@ async def remove_item(
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):
"""
Expand Down
20 changes: 0 additions & 20 deletions backend/controllers/models/plex.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
from typing import Optional

from pydantic import BaseModel, Field
from rich.console import Console
from rich.table import Table

console = Console()


class Account(BaseModel):
Expand Down Expand Up @@ -48,19 +44,3 @@ class PlexPayload(BaseModel):
Server: Server
Player: Player
Metadata: Metadata


def log_plex_payload(plex_payload):
table = Table(title="Plex Payload Details")

table.add_column("Field", style="bold cyan")
table.add_column("Value", style="bold magenta")

table.add_row("Event", plex_payload.event)
table.add_row("User", plex_payload.Account.title)
table.add_row("User ID", str(plex_payload.Account.id))
table.add_row("Media Title", plex_payload.Metadata.title)
table.add_row("Media Type", plex_payload.Metadata.type)
table.add_row("Year", str(plex_payload.Metadata.year))

console.print(table)
26 changes: 20 additions & 6 deletions backend/controllers/webhooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
get_imdbid_from_tmdb,
)
from program.media.item import MediaItem, Show
from requests import RequestException
from utils.logger import logger
from utils.request import get

Expand Down Expand Up @@ -41,12 +42,20 @@ async def overseerr(request: Request) -> Dict[str, Any]:

imdb_id = req.media.imdbId
if not imdb_id:
imdb_id = get_imdbid_from_tmdb(req.media.tmdbId)
try:
imdb_id = get_imdbid_from_tmdb(req.media.tmdbId)
except RequestException as e:
logger.error(f"Failed to get imdb_id from TMDB: {req.media.tmdbId}")
return {"success": False, "message": "Failed to get imdb_id from TMDB", "title": req.subject}
if not imdb_id:
logger.error(f"Failed to get imdb_id from TMDB: {req.media.tmdbId}")
return {"success": False, "message": "Failed to get imdb_id from TMDB", "title": req.subject}

overseerr: Overseerr = request.app.program.services[Overseerr]
if not overseerr.initialized:
logger.error("Overseerr not initialized")
return {"success": False, "message": "Overseerr not initialized", "title": req.subject}

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

if imdb_id in overseerr.recurring_items:
Expand All @@ -55,9 +64,14 @@ async def overseerr(request: Request) -> Dict[str, Any]:
else:
overseerr.recurring_items.add(imdb_id)

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)
try:
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)
except Exception as e:
logger.error(f"Failed to create item from imdb_id: {imdb_id}")
return {"success": False, "message": "Failed to create item from imdb_id", "title": req.subject}

return {"success": True}
2 changes: 1 addition & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def install_signal_handlers(self):

@contextlib.contextmanager
def run_in_thread(self):
thread = threading.Thread(target=self.run, name="Iceberg")
thread = threading.Thread(target=self.run, name="Riven")
thread.start()
try:
while not self.started:
Expand Down
9 changes: 6 additions & 3 deletions backend/program/content/overseerr.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def __init__(self):
self.settings = settings_manager.settings.content.overseerr
self.headers = {"X-Api-Key": self.settings.api_key}
self.initialized = self.validate()
self.run_once = False
if not self.initialized:
return
self.recurring_items = set()
Expand Down Expand Up @@ -52,8 +53,11 @@ def validate(self) -> bool:

def run(self):
"""Fetch new media from `Overseerr`"""
if self.settings.use_webhook:
return
if self.settings.use_webhook and not self.run_once:
if not hasattr(self, '_logged_webhook_message'):
logger.info("Webhook is enabled, but running Overseerr once before switching to webhook.")
self._logged_webhook_message = True
self.run_once = True

try:
response = get(
Expand Down Expand Up @@ -89,7 +93,6 @@ def run(self):
yield MediaItem({"imdb_id": imdb_id, "requested_by": self.key, "overseerr_id": mediaId})
except Exception as e:
logger.error(f"Error processing item {item}: {str(e)}")

continue

def get_imdb_id(self, data) -> str:
Expand Down
64 changes: 33 additions & 31 deletions backend/program/content/plex_watchlist.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Plex Watchlist Module"""

from typing import Generator, Union
from program.indexers.trakt import create_item_from_imdb_id

from program.indexers.trakt import create_item_from_imdb_id
from program.media.item import Episode, MediaItem, Movie, Season, Show
from program.settings.manager import settings_manager
from requests import HTTPError
Expand All @@ -29,22 +29,23 @@ def validate(self):
logger.warning("Plex Watchlists is set to disabled.")
return False
if self.settings.rss:
try:
response = ping(self.settings.rss)
response.raise_for_status()
self.rss_enabled = True
return True
except HTTPError as e:
if e.response.status_code == 404:
logger.warning("Plex RSS URL is Not Found. Please check your RSS URL in settings.")
else:
logger.warning(
f"Plex RSS URL is not reachable (HTTP status code: {e.response.status_code}). Falling back to using user Watchlist."
)
return True
except Exception as e:
logger.exception(f"Failed to validate Plex RSS URL: {e}")
return True
for rss_url in self.settings.rss:
try:
response = ping(rss_url)
response.raise_for_status()
self.rss_enabled = True
return True
except HTTPError as e:
if e.response.status_code == 404:
logger.warning(f"Plex RSS URL {rss_url} is Not Found. Please check your RSS URL in settings.")
else:
logger.warning(
f"Plex RSS URL {rss_url} is not reachable (HTTP status code: {e.response.status_code})."
)
except Exception as e:
logger.error(f"Failed to validate Plex RSS URL {rss_url}: {e}", exc_info=True)
logger.warning("None of the provided RSS URLs are reachable. Falling back to using user Watchlist.")
return False
return True

def run(self) -> Generator[Union[Movie, Show, Season, Episode], None, None]:
Expand All @@ -64,25 +65,26 @@ def run(self) -> Generator[Union[Movie, Show, Season, Episode], None, None]:
self.recurring_items.add(imdb_id)
try:
media_item: MediaItem = create_item_from_imdb_id(imdb_id)
if not media_item:
if media_item:
yield media_item
else:
logger.error(f"Failed to create media item from IMDb ID: {imdb_id}")
continue
yield media_item
except Exception as e:
logger.error(f"Error processing IMDb ID {imdb_id}: {e}")
continue
continue

def _get_items_from_rss(self) -> Generator[MediaItem, None, None]:
"""Fetch media from Plex RSS Feed."""
try:
response = get(self.settings.rss, timeout=60)
if not response.is_ok:
logger.error(f"Failed to fetch Plex RSS feed: HTTP {response.status_code}")
return
for item in response.data.items:
yield from self._extract_imdb_ids(item.guids)
except Exception as e:
logger.error(f"An unexpected error occurred while fetching Plex RSS feed: {e}")
"""Fetch media from Plex RSS Feeds."""
for rss_url in self.settings.rss:
try:
response = get(rss_url, timeout=60)
if not response.is_ok:
logger.error(f"Failed to fetch Plex RSS feed from {rss_url}: HTTP {response.status_code}")
continue
for item in response.data.items:
yield from self._extract_imdb_ids(item.guids)
except Exception as e:
logger.error(f"An unexpected error occurred while fetching Plex RSS feed from {rss_url}: {e}")

def _get_items_from_watchlist(self) -> Generator[MediaItem, None, None]:
"""Fetch media from Plex watchlist"""
Expand Down
Loading

0 comments on commit 8e3c63b

Please sign in to comment.