This commit is contained in:
@ -9,7 +9,7 @@ python = ">=3.11,<3.15"
|
|||||||
Django = "^4.0.3"
|
Django = "^4.0.3"
|
||||||
django-extensions = "^3.1.5"
|
django-extensions = "^3.1.5"
|
||||||
python-dateutil = "^2.8.2"
|
python-dateutil = "^2.8.2"
|
||||||
python-dotenv = "^0.20.0"
|
python-dotenv = ">=0.20.0,<2"
|
||||||
python-json-logger = "^2.0.2"
|
python-json-logger = "^2.0.2"
|
||||||
colorlog = "^6.6.0"
|
colorlog = "^6.6.0"
|
||||||
httpx = "<=0.27.2"
|
httpx = "<=0.27.2"
|
||||||
@ -43,6 +43,7 @@ ipython = "^8.14.0"
|
|||||||
pendulum = "^3"
|
pendulum = "^3"
|
||||||
trafilatura = "^1.6.3"
|
trafilatura = "^1.6.3"
|
||||||
django-imagekit = "^5.0.0"
|
django-imagekit = "^5.0.0"
|
||||||
|
django-mcp-server = "^0.5.7"
|
||||||
thefuzz = "^0.22.1"
|
thefuzz = "^0.22.1"
|
||||||
dataclass-wizard = "^0.35.0"
|
dataclass-wizard = "^0.35.0"
|
||||||
webdavclient3 = "^3.14.6"
|
webdavclient3 = "^3.14.6"
|
||||||
|
|||||||
518
vrobbler/apps/scrobbles/mcp.py
Normal file
518
vrobbler/apps/scrobbles/mcp.py
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
from mcp_server import MCPToolset
|
||||||
|
from scrobbles.models import Scrobble
|
||||||
|
from scrobbles.constants import LONG_PLAY_MEDIA
|
||||||
|
|
||||||
|
|
||||||
|
class ScrobbleToolset(MCPToolset):
|
||||||
|
def list_recent_scrobbles(
|
||||||
|
self,
|
||||||
|
days: int = 7,
|
||||||
|
media_type: str | None = None,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""List scrobbles from the last N days, optionally filtered by media type.
|
||||||
|
Valid media_type values: Video, Track, PodcastEpisode, SportEvent, Book,
|
||||||
|
Paper, VideoGame, BoardGame, GeoLocation, Trail, Beer, Puzzle, Food, Task,
|
||||||
|
WebPage, LifeEvent, Mood, BrickSet, Channel, BirdingLocation, DiscGolfCourse
|
||||||
|
"""
|
||||||
|
qs = (
|
||||||
|
Scrobble.objects.filter(user=self.request.user)
|
||||||
|
.select_related(
|
||||||
|
"video", "track", "book", "video_game", "board_game",
|
||||||
|
"beer", "puzzle", "food", "trail", "task", "web_page",
|
||||||
|
"life_event", "mood", "brick_set", "podcast_episode",
|
||||||
|
"sport_event", "geo_location", "birding_location",
|
||||||
|
"disc_golf_course", "channel",
|
||||||
|
)
|
||||||
|
.order_by("-timestamp")
|
||||||
|
)
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
qs = qs.filter(timestamp__gte=timezone.now() - datetime.timedelta(days=days))
|
||||||
|
if media_type:
|
||||||
|
qs = qs.filter(media_type=media_type)
|
||||||
|
qs = qs[:limit]
|
||||||
|
return [_scrobble_to_dict(s) for s in qs]
|
||||||
|
|
||||||
|
def get_scrobble(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a single scrobble by its UUID."""
|
||||||
|
try:
|
||||||
|
s = Scrobble.objects.filter(user=self.request.user).get(uuid=uuid)
|
||||||
|
except Scrobble.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _scrobble_to_dict(s)
|
||||||
|
|
||||||
|
def search_scrobbles(
|
||||||
|
self, query: str, media_type: str | None = None, limit: int = 20
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Search scrobbles by text in their log data or related media titles."""
|
||||||
|
from django.db.models import Q
|
||||||
|
qs = Scrobble.objects.filter(user=self.request.user).order_by("-timestamp")
|
||||||
|
if media_type:
|
||||||
|
qs = qs.filter(media_type=media_type)
|
||||||
|
qs = qs.filter(
|
||||||
|
Q(log__icontains=query)
|
||||||
|
| Q(video__title__icontains=query)
|
||||||
|
| Q(track__title__icontains=query)
|
||||||
|
| Q(book__title__icontains=query)
|
||||||
|
| Q(video_game__title__icontains=query)
|
||||||
|
| Q(board_game__title__icontains=query)
|
||||||
|
| Q(beer__title__icontains=query)
|
||||||
|
| Q(food__title__icontains=query)
|
||||||
|
| Q(trail__title__icontains=query)
|
||||||
|
| Q(task__title__icontains=query)
|
||||||
|
| Q(web_page__title__icontains=query)
|
||||||
|
| Q(life_event__title__icontains=query)
|
||||||
|
| Q(puzzle__title__icontains=query)
|
||||||
|
| Q(brick_set__title__icontains=query)
|
||||||
|
| Q(podcast_episode__title__icontains=query)
|
||||||
|
)[:limit]
|
||||||
|
return [_scrobble_to_dict(s) for s in qs]
|
||||||
|
|
||||||
|
def get_scrobbles_by_date(
|
||||||
|
self, date: str, media_type: str | None = None
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get scrobbles for a specific date (YYYY-MM-DD format)."""
|
||||||
|
import datetime
|
||||||
|
try:
|
||||||
|
dt = datetime.datetime.strptime(date, "%Y-%m-%d").date()
|
||||||
|
except ValueError:
|
||||||
|
return []
|
||||||
|
qs = Scrobble.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
timestamp__date=dt,
|
||||||
|
).order_by("-timestamp")
|
||||||
|
if media_type:
|
||||||
|
qs = qs.filter(media_type=media_type)
|
||||||
|
return [_scrobble_to_dict(s) for s in qs]
|
||||||
|
|
||||||
|
def get_in_progress_scrobbles(
|
||||||
|
self, media_type: str | None = None
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get scrobbles currently in progress (started but not finished).
|
||||||
|
These are long-play items like books, video games, brick sets, or tasks."""
|
||||||
|
qs = Scrobble.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
in_progress=True,
|
||||||
|
).order_by("-timestamp")
|
||||||
|
if media_type:
|
||||||
|
qs = qs.filter(media_type=media_type)
|
||||||
|
return [_scrobble_to_dict(s) for s in qs]
|
||||||
|
|
||||||
|
def get_long_play_scrobbles(
|
||||||
|
self, status: str = "in_progress", media_type: str | None = None
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get long-play scrobbles (books, video games, brick sets, tasks).
|
||||||
|
Status can be 'in_progress' or 'completed'."""
|
||||||
|
types = list(LONG_PLAY_MEDIA.values())
|
||||||
|
qs = Scrobble.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
media_type__in=types,
|
||||||
|
).order_by("-timestamp")
|
||||||
|
if media_type:
|
||||||
|
qs = qs.filter(media_type=media_type)
|
||||||
|
if status == "in_progress":
|
||||||
|
qs = qs.filter(in_progress=True)
|
||||||
|
elif status == "completed":
|
||||||
|
qs = qs.filter(in_progress=False)
|
||||||
|
return [_scrobble_to_dict(s) for s in qs]
|
||||||
|
|
||||||
|
|
||||||
|
class MediaToolset(MCPToolset):
|
||||||
|
def get_book(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a book by UUID."""
|
||||||
|
from books.models import Book
|
||||||
|
try:
|
||||||
|
b = Book.objects.get(uuid=uuid)
|
||||||
|
except Book.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(b, fields=["title", "pages", "language",
|
||||||
|
"first_publish_year", "isbn_13",
|
||||||
|
"publisher", "summary"])
|
||||||
|
|
||||||
|
def list_books(self, author: str | None = None, limit: int = 20) -> list[dict]:
|
||||||
|
"""List books, optionally filtered by author name."""
|
||||||
|
from books.models import Book
|
||||||
|
qs = Book.objects.all().order_by("title")
|
||||||
|
if author:
|
||||||
|
qs = qs.filter(authors__name__icontains=author)
|
||||||
|
return [_media_to_dict(b, fields=["title", "pages", "language",
|
||||||
|
"first_publish_year", "isbn_13",
|
||||||
|
"publisher"]) for b in qs[:limit]]
|
||||||
|
|
||||||
|
def get_track(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a music track by UUID."""
|
||||||
|
from music.models import Track
|
||||||
|
try:
|
||||||
|
t = Track.objects.select_related("artist_fk").get(uuid=uuid)
|
||||||
|
except Track.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(t, fields=["title", "base_run_time_seconds",
|
||||||
|
"artist_fk__name", "genre"])
|
||||||
|
|
||||||
|
def list_tracks(self, artist: str | None = None, limit: int = 20) -> list[dict]:
|
||||||
|
"""List music tracks, optionally filtered by artist name."""
|
||||||
|
from music.models import Track
|
||||||
|
qs = Track.objects.select_related("artist_fk").all().order_by("title")
|
||||||
|
if artist:
|
||||||
|
qs = qs.filter(artist_fk__name__icontains=artist)
|
||||||
|
return [_media_to_dict(t, fields=["title", "base_run_time_seconds",
|
||||||
|
"artist_fk__name", "genre"])
|
||||||
|
for t in qs[:limit]]
|
||||||
|
|
||||||
|
def get_video(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a video by UUID."""
|
||||||
|
from videos.models import Video
|
||||||
|
try:
|
||||||
|
v = Video.objects.select_related("tv_series", "channel").get(uuid=uuid)
|
||||||
|
except Video.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(v, fields=["title", "year", "overview",
|
||||||
|
"imdb_id", "imdb_rating",
|
||||||
|
"tv_series__name", "channel__title",
|
||||||
|
"season_number", "episode_number"])
|
||||||
|
|
||||||
|
def list_videos(self, series: str | None = None, limit: int = 20) -> list[dict]:
|
||||||
|
"""List videos, optionally filtered by series name."""
|
||||||
|
from videos.models import Video
|
||||||
|
qs = Video.objects.select_related("tv_series", "channel").all().order_by("title")
|
||||||
|
if series:
|
||||||
|
qs = qs.filter(tv_series__name__icontains=series)
|
||||||
|
return [_media_to_dict(v, fields=["title", "year", "overview",
|
||||||
|
"tv_series__name", "channel__title",
|
||||||
|
"season_number", "episode_number"])
|
||||||
|
for v in qs[:limit]]
|
||||||
|
|
||||||
|
def get_board_game(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a board game by UUID."""
|
||||||
|
from boardgames.models import BoardGame
|
||||||
|
try:
|
||||||
|
bg = BoardGame.objects.get(uuid=uuid)
|
||||||
|
except BoardGame.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(bg, fields=["title", "genre"])
|
||||||
|
|
||||||
|
def list_board_games(self, limit: int = 20) -> list[dict]:
|
||||||
|
"""List board games."""
|
||||||
|
from boardgames.models import BoardGame
|
||||||
|
qs = BoardGame.objects.all().order_by("title")[:limit]
|
||||||
|
return [_media_to_dict(bg, fields=["title", "genre"]) for bg in qs]
|
||||||
|
|
||||||
|
def get_podcast_episode(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a podcast episode by UUID."""
|
||||||
|
from podcasts.models import PodcastEpisode
|
||||||
|
try:
|
||||||
|
pe = PodcastEpisode.objects.select_related("podcast", "producer").get(
|
||||||
|
uuid=uuid
|
||||||
|
)
|
||||||
|
except PodcastEpisode.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(pe, fields=["title", "podcast__title",
|
||||||
|
"producer__name", "base_run_time_seconds"])
|
||||||
|
|
||||||
|
def get_beer(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a beer by UUID."""
|
||||||
|
from beers.models import Beer
|
||||||
|
try:
|
||||||
|
b = Beer.objects.select_related("style", "producer").get(uuid=uuid)
|
||||||
|
except Beer.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(b, fields=["title", "style__name",
|
||||||
|
"producer__name", "abv"])
|
||||||
|
|
||||||
|
def get_brick_set(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a brick set (LEGO) by UUID."""
|
||||||
|
from bricksets.models import BrickSet
|
||||||
|
try:
|
||||||
|
bs = BrickSet.objects.get(uuid=uuid)
|
||||||
|
except BrickSet.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(bs, fields=["title", "piece_count", "set_number"])
|
||||||
|
|
||||||
|
def get_video_game(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a video game by UUID."""
|
||||||
|
from videogames.models import VideoGame
|
||||||
|
try:
|
||||||
|
vg = VideoGame.objects.get(uuid=uuid)
|
||||||
|
except VideoGame.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(vg, fields=["title", "genre",
|
||||||
|
"base_run_time_seconds"])
|
||||||
|
|
||||||
|
def get_puzzle(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a puzzle by UUID."""
|
||||||
|
from puzzles.models import Puzzle
|
||||||
|
try:
|
||||||
|
p = Puzzle.objects.select_related("manufacturer").get(uuid=uuid)
|
||||||
|
except Puzzle.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(p, fields=["title", "piece_count",
|
||||||
|
"manufacturer__name"])
|
||||||
|
|
||||||
|
def get_web_page(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a web page by UUID."""
|
||||||
|
from webpages.models import WebPage
|
||||||
|
try:
|
||||||
|
wp = WebPage.objects.select_related("domain").get(uuid=uuid)
|
||||||
|
except WebPage.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(wp, fields=["title", "url", "domain__name"])
|
||||||
|
|
||||||
|
def get_task(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a task by UUID."""
|
||||||
|
from tasks.models import Task
|
||||||
|
try:
|
||||||
|
t = Task.objects.get(uuid=uuid)
|
||||||
|
except Task.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(t, fields=["title", "completed"])
|
||||||
|
|
||||||
|
def get_trail(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a trail by UUID."""
|
||||||
|
from trails.models import Trail
|
||||||
|
try:
|
||||||
|
t = Trail.objects.get(uuid=uuid)
|
||||||
|
except Trail.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(t, fields=["title", "genre", "base_run_time_seconds"])
|
||||||
|
|
||||||
|
def get_geo_location(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a geo location by UUID."""
|
||||||
|
from locations.models import GeoLocation
|
||||||
|
try:
|
||||||
|
gl = GeoLocation.objects.get(uuid=uuid)
|
||||||
|
except GeoLocation.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(gl, fields=["title", "latitude", "longitude"])
|
||||||
|
|
||||||
|
def get_life_event(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a life event by UUID."""
|
||||||
|
from lifeevents.models import LifeEvent
|
||||||
|
try:
|
||||||
|
le = LifeEvent.objects.get(uuid=uuid)
|
||||||
|
except LifeEvent.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(le, fields=["title", "event_date", "genre"])
|
||||||
|
|
||||||
|
def get_mood(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a mood entry by UUID."""
|
||||||
|
from moods.models import Mood
|
||||||
|
try:
|
||||||
|
m = Mood.objects.get(uuid=uuid)
|
||||||
|
except Mood.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(m, fields=["title", "mood_type", "mood_quality"])
|
||||||
|
|
||||||
|
def get_food(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a food entry by UUID."""
|
||||||
|
from foods.models import Food
|
||||||
|
try:
|
||||||
|
f = Food.objects.select_related("category").get(uuid=uuid)
|
||||||
|
except Food.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(f, fields=["title", "category__name"])
|
||||||
|
|
||||||
|
def get_bird_sighting(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a bird sighting by UUID."""
|
||||||
|
from birds.models import BirdSighting
|
||||||
|
try:
|
||||||
|
bs = BirdSighting.objects.select_related("bird").get(uuid=uuid)
|
||||||
|
except BirdSighting.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(bs, fields=["title", "bird__common_name",
|
||||||
|
"bird__scientific_name", "location"])
|
||||||
|
|
||||||
|
def get_disc_golf_course(self, uuid: str) -> dict | None:
|
||||||
|
"""Get a disc golf course by UUID."""
|
||||||
|
from discgolf.models import DiscGolfCourse
|
||||||
|
try:
|
||||||
|
dg = DiscGolfCourse.objects.get(uuid=uuid)
|
||||||
|
except DiscGolfCourse.DoesNotExist:
|
||||||
|
return None
|
||||||
|
return _media_to_dict(dg, fields=["title", "holes", "location"])
|
||||||
|
|
||||||
|
|
||||||
|
class StatsToolset(MCPToolset):
|
||||||
|
def get_scrobble_counts(self, days: int = 30) -> list[dict]:
|
||||||
|
"""Get scrobble counts grouped by media type for the last N days."""
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
cutoff = timezone.now() - datetime.timedelta(days=days)
|
||||||
|
qs = (
|
||||||
|
Scrobble.objects.filter(user=self.request.user, timestamp__gte=cutoff)
|
||||||
|
.values("media_type")
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
.order_by("-count")
|
||||||
|
)
|
||||||
|
return list(qs)
|
||||||
|
|
||||||
|
def get_top_media(
|
||||||
|
self, media_type: str, days: int = 30, limit: int = 10
|
||||||
|
) -> list[dict]:
|
||||||
|
"""Get the most-scrobbled items of a given media type in the last N days.
|
||||||
|
Valid media_type values: Video, Track, Book, BoardGame, Beer, etc."""
|
||||||
|
from django.utils import timezone
|
||||||
|
import datetime
|
||||||
|
from django.db.models import Count
|
||||||
|
|
||||||
|
cutoff = timezone.now() - datetime.timedelta(days=days)
|
||||||
|
|
||||||
|
rel_field = _media_type_to_rel_field(media_type)
|
||||||
|
if not rel_field:
|
||||||
|
return []
|
||||||
|
|
||||||
|
qs = (
|
||||||
|
Scrobble.objects.filter(
|
||||||
|
user=self.request.user,
|
||||||
|
media_type=media_type,
|
||||||
|
timestamp__gte=cutoff,
|
||||||
|
)
|
||||||
|
.values(rel_field)
|
||||||
|
.annotate(count=Count("id"))
|
||||||
|
.order_by("-count")
|
||||||
|
)[:limit]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
for row in qs:
|
||||||
|
obj_id = row[rel_field]
|
||||||
|
if obj_id is None:
|
||||||
|
continue
|
||||||
|
results.append({"id": obj_id, "count": row["count"]})
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _media_type_to_rel_field(media_type: str) -> str | None:
|
||||||
|
mapping = {
|
||||||
|
"Video": "video",
|
||||||
|
"Track": "track",
|
||||||
|
"PodcastEpisode": "podcast_episode",
|
||||||
|
"SportEvent": "sport_event",
|
||||||
|
"Book": "book",
|
||||||
|
"Paper": "paper",
|
||||||
|
"VideoGame": "video_game",
|
||||||
|
"BoardGame": "board_game",
|
||||||
|
"GeoLocation": "geo_location",
|
||||||
|
"Trail": "trail",
|
||||||
|
"Beer": "beer",
|
||||||
|
"Puzzle": "puzzle",
|
||||||
|
"Food": "food",
|
||||||
|
"Task": "task",
|
||||||
|
"WebPage": "web_page",
|
||||||
|
"LifeEvent": "life_event",
|
||||||
|
"Mood": "mood",
|
||||||
|
"BrickSet": "brick_set",
|
||||||
|
"Channel": "channel",
|
||||||
|
"BirdingLocation": "birding_location",
|
||||||
|
"DiscGolfCourse": "disc_golf_course",
|
||||||
|
}
|
||||||
|
return mapping.get(media_type)
|
||||||
|
|
||||||
|
|
||||||
|
def _scrobble_to_dict(s: Scrobble) -> dict:
|
||||||
|
result = {
|
||||||
|
"uuid": str(s.uuid),
|
||||||
|
"media_type": s.media_type,
|
||||||
|
"timestamp": s.timestamp.isoformat() if s.timestamp else None,
|
||||||
|
"stop_timestamp": s.stop_timestamp.isoformat() if s.stop_timestamp else None,
|
||||||
|
"in_progress": s.in_progress,
|
||||||
|
"played_to_completion": s.played_to_completion,
|
||||||
|
"source": s.source,
|
||||||
|
"visibility": s.visibility,
|
||||||
|
"timezone": s.timezone,
|
||||||
|
}
|
||||||
|
if s.log:
|
||||||
|
result["log"] = s.log
|
||||||
|
|
||||||
|
rel = _scrobble_related_to_dict(s)
|
||||||
|
if rel:
|
||||||
|
result["media"] = rel
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _scrobble_related_to_dict(s: Scrobble) -> dict | None:
|
||||||
|
if s.video:
|
||||||
|
return _media_to_dict(s.video, fields=["title", "year", "imdb_id",
|
||||||
|
"imdb_rating"])
|
||||||
|
if s.track:
|
||||||
|
return _media_to_dict(s.track, fields=["title",
|
||||||
|
"base_run_time_seconds"])
|
||||||
|
if s.book:
|
||||||
|
return _media_to_dict(s.book, fields=["title", "pages"])
|
||||||
|
if s.video_game:
|
||||||
|
return _media_to_dict(s.video_game, fields=["title",
|
||||||
|
"base_run_time_seconds"])
|
||||||
|
if s.board_game:
|
||||||
|
return _media_to_dict(s.board_game, fields=["title"])
|
||||||
|
if s.beer:
|
||||||
|
return _media_to_dict(s.beer, fields=["title"])
|
||||||
|
if s.puzzle:
|
||||||
|
return _media_to_dict(s.puzzle, fields=["title", "piece_count"])
|
||||||
|
if s.food:
|
||||||
|
return _media_to_dict(s.food, fields=["title"])
|
||||||
|
if s.trail:
|
||||||
|
return _media_to_dict(s.trail, fields=["title",
|
||||||
|
"base_run_time_seconds"])
|
||||||
|
if s.task:
|
||||||
|
return _media_to_dict(s.task, fields=["title"])
|
||||||
|
if s.web_page:
|
||||||
|
return _media_to_dict(s.web_page, fields=["title", "url"])
|
||||||
|
if s.life_event:
|
||||||
|
return _media_to_dict(s.life_event, fields=["title", "event_date"])
|
||||||
|
if s.mood:
|
||||||
|
return _media_to_dict(s.mood, fields=["title"])
|
||||||
|
if s.brick_set:
|
||||||
|
return _media_to_dict(s.brick_set, fields=["title", "set_number"])
|
||||||
|
if s.podcast_episode:
|
||||||
|
return _media_to_dict(s.podcast_episode, fields=["title"])
|
||||||
|
if s.sport_event:
|
||||||
|
return {"title": str(s.sport_event)}
|
||||||
|
if s.geo_location:
|
||||||
|
return _media_to_dict(s.geo_location, fields=["title", "latitude",
|
||||||
|
"longitude"])
|
||||||
|
if s.birding_location:
|
||||||
|
return _media_to_dict(s.birding_location, fields=["title"])
|
||||||
|
if s.disc_golf_course:
|
||||||
|
return _media_to_dict(s.disc_golf_course, fields=["title", "holes"])
|
||||||
|
if s.channel:
|
||||||
|
return _media_to_dict(s.channel, fields=["title"])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _media_to_dict(obj, fields: list[str] | None = None) -> dict:
|
||||||
|
if obj is None:
|
||||||
|
return {}
|
||||||
|
result = {}
|
||||||
|
if hasattr(obj, "uuid"):
|
||||||
|
result["uuid"] = str(obj.uuid)
|
||||||
|
if hasattr(obj, "title"):
|
||||||
|
result["title"] = obj.title
|
||||||
|
|
||||||
|
if fields is None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
resolved = _resolve_fields(obj, fields)
|
||||||
|
for k, v in resolved.items():
|
||||||
|
if k not in result:
|
||||||
|
result[k] = v
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_fields(obj, fields: list[str]) -> dict:
|
||||||
|
result = {}
|
||||||
|
for field in fields:
|
||||||
|
parts = field.split("__")
|
||||||
|
val = obj
|
||||||
|
try:
|
||||||
|
for part in parts:
|
||||||
|
val = getattr(val, part)
|
||||||
|
except AttributeError:
|
||||||
|
continue
|
||||||
|
if val is not None:
|
||||||
|
if hasattr(val, "all"):
|
||||||
|
val = [str(v) for v in val.all()]
|
||||||
|
result[field] = val
|
||||||
|
return result
|
||||||
@ -236,6 +236,7 @@ INSTALLED_APPS = [
|
|||||||
"allauth.account",
|
"allauth.account",
|
||||||
"allauth.socialaccount",
|
"allauth.socialaccount",
|
||||||
"django_celery_results",
|
"django_celery_results",
|
||||||
|
"mcp_server",
|
||||||
]
|
]
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
@ -329,6 +330,13 @@ REST_FRAMEWORK = {
|
|||||||
"PAGE_SIZE": 200,
|
"PAGE_SIZE": 200,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DJANGO_MCP_AUTHENTICATION_CLASSES = [
|
||||||
|
"oauth2_provider.contrib.rest_framework.OAuth2Authentication",
|
||||||
|
"rest_framework.authentication.TokenAuthentication",
|
||||||
|
"rest_framework.authentication.SessionAuthentication",
|
||||||
|
]
|
||||||
|
DJANGO_MCP_GET_SERVER_INSTRUCTIONS_TOOL = True
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = "/"
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
|
||||||
AUTH_PASSWORD_VALIDATORS = [
|
AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
|||||||
@ -188,6 +188,7 @@ urlpatterns = [
|
|||||||
path("", include(people_urls, namespace="people")),
|
path("", include(people_urls, namespace="people")),
|
||||||
path("", include(charts_urls, namespace="charts")),
|
path("", include(charts_urls, namespace="charts")),
|
||||||
path("", include(trends_urls, namespace="trends")),
|
path("", include(trends_urls, namespace="trends")),
|
||||||
|
path("", include("mcp_server.urls")),
|
||||||
path("", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"),
|
path("", scrobbles_views.RecentScrobbleList.as_view(), name="vrobbler-home"),
|
||||||
]
|
]
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
Reference in New Issue
Block a user