implement JellyStat API for watch history, genre summary, and user summary; add PostgreSQL connection pool and update requirements
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
-- ============================================================================
|
||||
-- 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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user