Skip to content

Commit

Permalink
Release 0.7.3 (#439)
Browse files Browse the repository at this point in the history
* fix: testing new changes

* feat: improve torrentio scraping

* feat: merge changes from 0x

* feat: bump version

* fix: remove scraping should submit

* fuckin dad..

* fix: sort final streams at end of scraping

* feat: add enrichment

* fix: typos in jackett and prowlarr

* fix: move tmdb init to indexers module. couple other slight tweaks

* test: added debrid matching test

* fix: removed username from debrid log

* fix: add movie to cold boot indexing

* This just magically disappeared

* fix: sort imports

* feat: add tracemalloc

* Patch changes trakt (#452)

* fix: copy incoming item attributes to new item through trakt
Tidy: Add more explicitness to the run definition *Spoked

* fix: ordering of season and episode

---------

Co-authored-by: Administrator <[email protected]>

* feat: update enrichment

---------

Co-authored-by: Spoked <Spoked@localhost>
Co-authored-by: Gaisberg <None>
Co-authored-by: Matthew Adams <[email protected]>
Co-authored-by: Administrator <[email protected]>
  • Loading branch information
4 people authored Jun 20, 2024
1 parent adf54f4 commit 6f30125
Show file tree
Hide file tree
Showing 23 changed files with 451 additions and 181 deletions.
11 changes: 9 additions & 2 deletions .github/workflows/frontend-battery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,12 @@ jobs:
run: pnpm install
working-directory: ./frontend

- run: pnpm lint
working-directory: ./frontend
- uses: dorny/paths-filter@v2
id: changes
with:
filters: |
modified:
- 'frontend/**'
- name: Run lint
if: steps.changes.outputs.modified == 'true'
run: pnpm lint
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.2
0.7.3
2 changes: 1 addition & 1 deletion backend/controllers/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,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": "riven", "requested_at": datetime.now()})
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"}
Expand Down
4 changes: 1 addition & 3 deletions backend/controllers/tmdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@
from urllib.parse import urlencode

from fastapi import APIRouter, Depends
from program.indexers.tmdb import TMDB
from program.indexers.tmdb import tmdb

router = APIRouter(
prefix="/tmdb",
tags=["tmdb"],
responses={404: {"description": "Not found"}},
)

tmdb = TMDB()


def dict_to_query_string(params: dict):
filtered_params = {k: v for k, v in params.items() if v is not None}
Expand Down
19 changes: 4 additions & 15 deletions backend/controllers/webhooks.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
from datetime import datetime
from typing import Any, Dict

import pydantic
from fastapi import APIRouter, Request
from program.content.overseerr import Overseerr
from program.indexers.trakt import (
TraktIndexer,
create_item_from_imdb_id,
get_imdbid_from_tmdb,
)
from program.indexers.trakt import TraktIndexer, 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

from .models.overseerr import OverseerrWebhook

Expand Down Expand Up @@ -56,22 +50,17 @@ async def overseerr(request: Request) -> Dict[str, Any]:
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:
logger.log("API", "Request already in queue", {"imdb_id": imdb_id})
return {"success": False, "message": "Request already in queue", "title": req.subject}
return {"success": True, "message": "Request already in queue", "title": req.subject}
else:
overseerr.recurring_items.add(imdb_id)

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)
request.app.program.add_to_queue(new_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}
return {"success": True, "message": f"Added {imdb_id} to queue"}
14 changes: 10 additions & 4 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse
import contextlib
import os
import signal
import sys
import threading
import time
Expand Down Expand Up @@ -92,7 +93,15 @@ def run_in_thread(self):
raise e
finally:
self.should_exit = True
sys.exit(0)

def signal_handler(sig, frame):
logger.log('PROGRAM','Exiting Gracefully.')
app.program.stop()
sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

config = uvicorn.Config(app, host="0.0.0.0", port=8080, log_config=None)
server = Server(config=config)
Expand All @@ -101,12 +110,9 @@ def run_in_thread(self):
try:
app.program.start()
app.program.run()
except KeyboardInterrupt:
pass
except Exception as e:
logger.error(f"Error in main thread: {e}")
logger.exception(traceback.format_exc())
finally:
app.program.stop()
logger.critical("Server has been stopped")
sys.exit(0)
sys.exit(0)
8 changes: 4 additions & 4 deletions backend/program/content/overseerr.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ def validate(self) -> bool:
def run(self):
"""Fetch new media from `Overseerr`"""
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
logger.info("Webhook is enabled, but running Overseerr once before switching to webhook.")
self.run_once = True

if self.run_once:
return

try:
response = get(
Expand Down Expand Up @@ -139,7 +140,6 @@ def get_imdb_id(self, data) -> str:
except Exception as e:
logger.error(f"Error fetching alternate ID: {str(e)}")
continue
return

@staticmethod
def delete_request(mediaId: int) -> bool:
Expand Down
15 changes: 5 additions & 10 deletions backend/program/content/plex_watchlist.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from typing import Generator, Union

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 Down Expand Up @@ -59,20 +58,16 @@ def run(self) -> Generator[Union[Movie, Show, Season, Episode], None, None]:

new_items = watchlist_items | rss_items

items = [
MediaItem({"imdb_id": imdb_id, "requested_by": self.key})
for imdb_id in new_items if imdb_id not in self.recurring_items
]
for imdb_id in new_items:
if imdb_id in self.recurring_items:
continue
self.recurring_items.add(imdb_id)
try:
media_item: MediaItem = create_item_from_imdb_id(imdb_id)
if media_item:
yield media_item
else:
logger.error(f"Failed to create media item from IMDb ID: {imdb_id}")
except Exception as e:
logger.error(f"Error processing IMDb ID {imdb_id}: {e}")
continue

yield items
def _get_items_from_rss(self) -> Generator[MediaItem, None, None]:
"""Fetch media from Plex RSS Feeds."""
for rss_url in self.settings.rss:
Expand Down
55 changes: 50 additions & 5 deletions backend/program/content/trakt.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Trakt content module"""
import re
import time
from types import SimpleNamespace
from urllib.parse import urlencode, urlparse
Expand Down Expand Up @@ -155,12 +156,11 @@ def _get_list(self, list_items: list) -> list:
return []
imdb_ids = []
for url in list_items:
match = regex.match(r'https://trakt.tv/users/([^/]+)/lists/([^/]+)', url)
if not match:
user, list_name = _extract_user_list_from_url(url)
if not user or not list_name:
logger.error(f"Invalid list URL: {url}")
continue
user, list_name = match.groups()
list_name = urlparse(url).path.split('/')[-1]

items = get_user_list(self.api_url, self.headers, user, list_name)
for item in items:
if hasattr(item, "movie"):
Expand Down Expand Up @@ -312,4 +312,49 @@ def get_popular_items(api_url, headers, media_type, limit=10):
def get_favorited_items(api_url, headers, user, limit=10):
"""Get favorited items from Trakt with pagination support."""
url = f"{api_url}/users/{user}/favorites"
return _fetch_data(url, headers, {"limit": limit})
return _fetch_data(url, headers, {"limit": limit})


def _extract_user_list_from_url(url) -> tuple:
"""Extract user and list name from Trakt URL"""

def match_full_url(url: str) -> tuple:
"""Helper function to match full URL format"""
match = patterns["user_list"].match(url)
if match:
return match.groups()
return None, None

# First try to match the original URL
user, list_name = match_full_url(url)
if user and list_name:
return user, list_name

# If it's a short URL, resolve it and try to match again
match = patterns["short_list"].match(url)
if match:
full_url = _resolve_short_url(url)
if full_url:
user, list_name = match_full_url(full_url)
if user and list_name:
return user, list_name

return None, None

def _resolve_short_url(short_url) -> str or None:
"""Resolve short URL to full URL"""
try:
response = get(short_url, additional_headers={"Content-Type": "application/json", "Accept": "text/html"})
if response.is_ok:
return response.response.url
else:
logger.error(f"Failed to resolve short URL: {short_url} (with status code: {response.status_code})")
return None
except RequestException as e:
logger.error(f"Error resolving short URL: {str(e)}")
return None

patterns: dict[str, re.Pattern] = {
"user_list": re.compile(r'https://trakt.tv/users/([^/]+)/lists/([^/]+)'),
"short_list": re.compile(r'https://trakt.tv/lists/\d+')
}
14 changes: 7 additions & 7 deletions backend/program/downloaders/realdebrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,6 @@ def validate(self) -> bool:
response = ping(f"{RD_BASE_URL}/user", additional_headers=self.auth_headers, proxies=self.proxy)
if response.ok:
user_info = response.json()
username = user_info.get("username", "")
premium_status = "Premium" if user_info.get("premium", 0) > 0 else "Not Premium"
expiration = user_info.get("expiration", "")
expiration_datetime = datetime.fromisoformat(expiration.replace('Z', '+00:00')).replace(tzinfo=None)
time_left = expiration_datetime - datetime.utcnow().replace(tzinfo=None)
Expand All @@ -81,11 +79,9 @@ def validate(self) -> bool:
expiration_message = "Your account expires soon."

if user_info.get("type", "") != "premium":
logger.log("DEBRID", "You are not a premium member.")
logger.error("You are not a premium member.")
return False
else:
logger.log("DEBRID", f"Hello {username}, your account is {premium_status}.")
logger.log("DEBRID", f"Expiration: {expiration_datetime}")
logger.log("DEBRID", expiration_message)

return user_info.get("premium", 0) > 0
Expand All @@ -99,14 +95,18 @@ def validate(self) -> bool:
def run(self, item: MediaItem) -> Generator[MediaItem, None, None]:
"""Download media item from real-debrid.com"""
if (item.file and item.folder):
yield None
return
if not self.is_cached(item):
if isinstance(item, Season) and item.scraped_times > 1:
if isinstance(item, Season):
res = [e for e in item.episodes]
yield res
if isinstance(item, Show) and item.scraped_times > 0:
return
if isinstance(item, Show):
res = [s for s in item.seasons]
yield res
return
yield None
return
if not self._is_downloaded(item):
self._download_item(item)
Expand Down
3 changes: 3 additions & 0 deletions backend/program/indexers/tmdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,6 @@ def getTVSeasonEpisodeDetails(
f"An error occurred while getting TV season episode details: {str(e)}"
)
return None


tmdb = TMDB()
31 changes: 26 additions & 5 deletions backend/program/indexers/trakt.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Trakt updater module"""

from datetime import datetime, timedelta
from typing import Generator, List, Optional
from typing import Generator, List, Optional, Union

from program.media.item import Episode, MediaItem, Movie, Season, Show
from program.settings.manager import settings_manager
Expand All @@ -21,20 +21,36 @@ def __init__(self):
self.initialized = True
self.settings = settings_manager.settings.indexer

def run(self, item: MediaItem) -> Generator[MediaItem, None, None]:
def copy_items(self, itema: MediaItem, itemb: MediaItem):
if isinstance(itema, Show) and isinstance(itemb, Show):
for (seasona, seasonb) in zip(itema.seasons, itemb.seasons):
for (episodea, episodeb) in zip(seasona.episodes, seasonb.episodes):
episodeb.set("update_folder", episodea.update_folder)
episodeb.set("symlinked", episodea.symlinked)
episodeb.set("is_anime", episodea.is_anime)
elif isinstance(itema, Movie) and isinstance(itemb, Movie):
itemb.set("update_folder", itema.update_folder)
itemb.set("symlinked", itema.symlinked)
itemb.set("is_anime", itema.is_anime)
return itemb

def run(self, in_item: MediaItem) -> Generator[Union[Movie, Show, Season, Episode], None, None]:
"""Run the Trakt indexer for the given item."""
if not item:
if not in_item:
logger.error("Item is None")
return
if (imdb_id := item.imdb_id) is None:
if (imdb_id := in_item.imdb_id) is None:
logger.error(f"Item {item.log_string} does not have an imdb_id, cannot index it")
return

item = create_item_from_imdb_id(imdb_id)

if not isinstance(item, MediaItem):
logger.error(f"Failed to get item from imdb_id: {imdb_id}")
return
if isinstance(item, Show):
self._add_seasons_to_show(item, imdb_id)
item = self.copy_items(in_item, item)
item.indexed_at = datetime.now()
yield item

Expand Down Expand Up @@ -94,13 +110,18 @@ def _map_item_from_data(data, item_type: str, show_genres: List[str] = None) ->
"tvdb_id": getattr(data.ids, "tvdb", None),
"tmdb_id": getattr(data.ids, "tmdb", None),
"genres": genres,
"is_anime": "anime" in genres if genres else False,
"network": getattr(data, "network", None),
"country": getattr(data, "country", None),
"language": getattr(data, "language", None),
"requested_at": datetime.now(),
}

item["is_anime"] = (
("anime" in genres or "animation" in genres) if genres
and item["country"] in ("jp", "kr")
else False
)

match item_type:
case "movie":
return Movie(item)
Expand Down
Loading

0 comments on commit 6f30125

Please sign in to comment.