224 lines
8.4 KiB
PL/PgSQL
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;
|
|
$$; |