Files
Agents/gateway/jellystat/startup-functions.sql
T

224 lines
8.4 KiB
PL/PgSQL

-- ============================================================================
-- JellyStat API Functions
-- Parameterized database functions callable by the API layer as:
-- SELECT * FROM fn_user_watch_history('user_id_here', 10080);
-- SELECT * FROM fn_user_genre_summary('user_id_here', 10080);
-- SELECT * FROM fn_user_summary('user_id_here');
-- ============================================================================
-- ----------------------------------------------------------------------------
-- 1. User Watch History
-- Returns every distinct title watched in the last N minutes,
-- grouped and summed by title, ordered by most-watched first.
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.fn_user_watch_history(
p_user_id TEXT,
p_minutes INTEGER DEFAULT 10080 -- 7 days in minutes
)
RETURNS TABLE(
title TEXT,
watch_time_sec NUMERIC,
media_type TEXT
)
LANGUAGE sql
STABLE
AS $$
SELECT
COALESCE(a."SeriesName", a."NowPlayingItemName") AS title,
SUM(a."PlaybackDuration")::NUMERIC AS watch_time_sec,
CASE
WHEN a."SeriesName" IS NOT NULL THEN 'series'
ELSE 'movie'
END AS media_type
FROM jf_playback_activity a
WHERE a."UserId" = p_user_id
AND a."ActivityDateInserted"
>= NOW() - (p_minutes * INTERVAL '1 minute')
GROUP BY
COALESCE(a."SeriesName", a."NowPlayingItemName"),
CASE WHEN a."SeriesName" IS NOT NULL THEN 'series' ELSE 'movie' END
ORDER BY watch_time_sec DESC;
$$;
-- ----------------------------------------------------------------------------
-- 2. Genre Summary
-- Returns total watch time per genre for a user over the last N minutes.
-- Resolves genres for both movies (directly on the item) and series
-- episodes (via jf_library_episodes → jf_library_items chain).
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.fn_user_genre_summary(
p_user_id TEXT,
p_minutes INTEGER DEFAULT 10080
)
RETURNS TABLE(
genre TEXT,
watch_time_sec NUMERIC
)
LANGUAGE sql
STABLE
AS $$
WITH movie_genres AS (
-- Movies: join playback directly to library_items on NowPlayingItemId
SELECT
genre_item.value AS genre,
SUM(a."PlaybackDuration") AS watch_time_sec
FROM jf_playback_activity a
JOIN jf_library_items i
ON i."Id" = a."NowPlayingItemId"
CROSS JOIN LATERAL jsonb_array_elements_text(i."Genres") AS genre_item(value)
WHERE a."UserId" = p_user_id
AND a."SeriesName" IS NULL -- movies only
AND a."ActivityDateInserted"
>= NOW() - (p_minutes * INTERVAL '1 minute')
AND i."Genres" IS NOT NULL
AND jsonb_array_length(i."Genres") > 0
GROUP BY genre_item.value
),
series_genres AS (
-- Series: playback → episodes → series item → genres
SELECT
genre_item.value AS genre,
SUM(a."PlaybackDuration") AS watch_time_sec
FROM jf_playback_activity a
JOIN jf_library_episodes e
ON e."EpisodeId" = a."EpisodeId"
JOIN jf_library_items i
ON i."Id" = e."SeriesId"
CROSS JOIN LATERAL jsonb_array_elements_text(i."Genres") AS genre_item(value)
WHERE a."UserId" = p_user_id
AND a."SeriesName" IS NOT NULL -- TV episodes only
AND a."ActivityDateInserted"
>= NOW() - (p_minutes * INTERVAL '1 minute')
AND i."Genres" IS NOT NULL
AND jsonb_array_length(i."Genres") > 0
GROUP BY genre_item.value
),
combined AS (
SELECT genre, watch_time_sec FROM movie_genres
UNION ALL
SELECT genre, watch_time_sec FROM series_genres
)
SELECT
genre,
SUM(watch_time_sec)::NUMERIC AS watch_time_sec
FROM combined
GROUP BY genre
ORDER BY watch_time_sec DESC;
$$;
-- ----------------------------------------------------------------------------
-- 3. User Summary
-- One-shot dashboard: all-time stats + recent windows + top genres.
-- Returns key-value rows that the API trivially converts to a JSON object
-- with Object.fromEntries() or similar.
-- ----------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.fn_user_summary(
p_user_id TEXT
)
RETURNS TABLE(
metric TEXT,
value JSONB
)
LANGUAGE sql
STABLE
AS $$
-- total_watch_time (all time)
SELECT 'total_watch_time'::TEXT AS metric,
to_jsonb(COALESCE(SUM("PlaybackDuration"), 0)::NUMERIC) AS value
FROM jf_playback_activity
WHERE "UserId" = p_user_id
UNION ALL
-- most_watched_series (by total watch time)
SELECT 'most_watched_series'::TEXT AS metric,
COALESCE(
(SELECT to_jsonb("SeriesName")
FROM jf_playback_activity
WHERE "UserId" = p_user_id
AND "SeriesName" IS NOT NULL
GROUP BY "SeriesName"
ORDER BY SUM("PlaybackDuration") DESC
LIMIT 1),
'null'::JSONB
) AS value
UNION ALL
-- most_watched_movie (by total watch time)
SELECT 'most_watched_movie'::TEXT AS metric,
COALESCE(
(SELECT to_jsonb("NowPlayingItemName")
FROM jf_playback_activity
WHERE "UserId" = p_user_id
AND "SeriesName" IS NULL
GROUP BY "NowPlayingItemName"
ORDER BY SUM("PlaybackDuration") DESC
LIMIT 1),
'null'::JSONB
) AS value
UNION ALL
-- total_watch_time_last_month (last 30 days)
SELECT 'total_last_30d'::TEXT AS metric,
to_jsonb(COALESCE(SUM("PlaybackDuration"), 0)::NUMERIC) AS value
FROM jf_playback_activity
WHERE "UserId" = p_user_id
AND "ActivityDateInserted" >= NOW() - INTERVAL '30 days'
UNION ALL
-- total_watch_time_last_week (last 7 days)
SELECT 'total_last_7d'::TEXT AS metric,
to_jsonb(COALESCE(SUM("PlaybackDuration"), 0)::NUMERIC) AS value
FROM jf_playback_activity
WHERE "UserId" = p_user_id
AND "ActivityDateInserted" >= NOW() - INTERVAL '7 days'
UNION ALL
-- top_genres (top 3 all-time, as a JSON array)
SELECT 'top_genres'::TEXT AS metric,
COALESCE(
(SELECT jsonb_agg(genre ORDER BY watch_time_sec DESC)
FROM (
SELECT genre, SUM(watch_time_sec) AS watch_time_sec
FROM (
-- movies
SELECT
genre_item.value AS genre,
SUM(a."PlaybackDuration") AS watch_time_sec
FROM jf_playback_activity a
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
CROSS JOIN LATERAL jsonb_array_elements_text(i."Genres") AS genre_item(value)
WHERE a."UserId" = p_user_id
AND a."SeriesName" IS NULL
AND i."Genres" IS NOT NULL
AND jsonb_array_length(i."Genres") > 0
GROUP BY genre_item.value
UNION ALL
-- series
SELECT
genre_item.value AS genre,
SUM(a."PlaybackDuration") AS watch_time_sec
FROM jf_playback_activity a
JOIN jf_library_episodes e ON e."EpisodeId" = a."EpisodeId"
JOIN jf_library_items i ON i."Id" = e."SeriesId"
CROSS JOIN LATERAL jsonb_array_elements_text(i."Genres") AS genre_item(value)
WHERE a."UserId" = p_user_id
AND a."SeriesName" IS NOT NULL
AND i."Genres" IS NOT NULL
AND jsonb_array_length(i."Genres") > 0
GROUP BY genre_item.value
) combined
GROUP BY genre
ORDER BY SUM(watch_time_sec) DESC
LIMIT 3
) top3
),
'[]'::JSONB
) AS value;
$$;