mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-01-18 16:27:20 +01:00
Added Genre stats to users page #292
Added Genres to items table enhanced db-helper to add group by fixed bug in fb-helper where null rows triggered a not count prop error fixed issue where no plays on user screen attempted to open the item page updated translations for genres
This commit is contained in:
@@ -104,6 +104,7 @@ async function query({
|
||||
joins = [],
|
||||
where = [],
|
||||
values = [],
|
||||
group_by = [],
|
||||
order_by = "Id",
|
||||
sort_order = "desc",
|
||||
pageNumber = 1,
|
||||
@@ -120,9 +121,9 @@ async function query({
|
||||
const joinConditions = join.conditions
|
||||
.map((condition, index) => {
|
||||
const conjunction = index === 0 ? "" : condition.type ? condition.type.toUpperCase() : "AND";
|
||||
return `${conjunction} ${wrapField(condition.first)} ${condition.operator} ${
|
||||
condition.second ? wrapField(condition.second) : `${condition.value}`
|
||||
}`;
|
||||
return `${conjunction} ${condition.wrap == false ? condition.first : wrapField(condition.first)} ${
|
||||
condition.operator
|
||||
} ${condition.second ? wrapField(condition.second) : `${condition.value}`}`;
|
||||
})
|
||||
.join(" ");
|
||||
const joinQuery = ` ${join.type.toUpperCase()} JOIN ${join.table} AS ${join.alias} ON ${joinConditions}`;
|
||||
@@ -137,6 +138,12 @@ async function query({
|
||||
countQuery += ` WHERE ${whereClause}`;
|
||||
}
|
||||
|
||||
// Add group by
|
||||
if (group_by.length > 0) {
|
||||
query += ` GROUP BY ${group_by.map(wrapField).join(", ")}`;
|
||||
countQuery += ` GROUP BY ${group_by.map(wrapField).join(", ")}`;
|
||||
}
|
||||
|
||||
// Add order by and pagination
|
||||
query += ` ORDER BY ${wrapField(order_by)} ${sort_order}`;
|
||||
query += ` LIMIT ${pageSize} OFFSET ${(pageNumber - 1) * pageSize}`;
|
||||
@@ -146,7 +153,7 @@ async function query({
|
||||
|
||||
// Count total rows
|
||||
const countResult = await client.query(countQuery, values);
|
||||
const totalRows = parseInt(countResult.rows[0].count, 10);
|
||||
const totalRows = parseInt(countResult.rows.length > 0 ? countResult.rows[0].count : 0, 10);
|
||||
|
||||
// Return the structured response
|
||||
return {
|
||||
|
||||
@@ -162,7 +162,7 @@ class EmbyAPI {
|
||||
"X-MediaBrowser-Token": this.config.JF_API_KEY,
|
||||
},
|
||||
params: {
|
||||
fields: "MediaSources,DateCreated",
|
||||
fields: "MediaSources,DateCreated,Genres",
|
||||
startIndex: startIndex,
|
||||
recursive: recursive,
|
||||
limit: limit,
|
||||
@@ -230,7 +230,7 @@ class EmbyAPI {
|
||||
"X-MediaBrowser-Token": this.config.JF_API_KEY,
|
||||
},
|
||||
params: {
|
||||
fields: "MediaSources,DateCreated",
|
||||
fields: "MediaSources,DateCreated,Genres",
|
||||
startIndex: startIndex,
|
||||
recursive: recursive,
|
||||
limit: limit,
|
||||
@@ -413,7 +413,7 @@ class EmbyAPI {
|
||||
"X-MediaBrowser-Token": this.config.JF_API_KEY,
|
||||
},
|
||||
params: {
|
||||
fields: "MediaSources,DateCreated",
|
||||
fields: "MediaSources,DateCreated,Genres",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ class JellyfinAPI {
|
||||
"X-MediaBrowser-Token": this.config.JF_API_KEY,
|
||||
},
|
||||
params: {
|
||||
fields: "MediaSources,DateCreated",
|
||||
fields: "MediaSources,DateCreated,Genres",
|
||||
startIndex: startIndex,
|
||||
recursive: recursive,
|
||||
limit: limit,
|
||||
@@ -228,7 +228,7 @@ class JellyfinAPI {
|
||||
"X-MediaBrowser-Token": this.config.JF_API_KEY,
|
||||
},
|
||||
params: {
|
||||
fields: "MediaSources,DateCreated",
|
||||
fields: "MediaSources,DateCreated,Genres",
|
||||
startIndex: startIndex,
|
||||
recursive: recursive,
|
||||
limit: limit,
|
||||
@@ -411,7 +411,7 @@ class JellyfinAPI {
|
||||
"X-MediaBrowser-Token": this.config.JF_API_KEY,
|
||||
},
|
||||
params: {
|
||||
fields: "MediaSources,DateCreated",
|
||||
fields: "MediaSources,DateCreated,Genres",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
25
backend/migrations/094_jf_library_items_table_add_genres.js
Normal file
25
backend/migrations/094_jf_library_items_table_add_genres.js
Normal file
@@ -0,0 +1,25 @@
|
||||
exports.up = async function (knex) {
|
||||
try {
|
||||
const hasTable = await knex.schema.hasTable("jf_library_items");
|
||||
if (hasTable) {
|
||||
await knex.schema.alterTable("jf_library_items", function (table) {
|
||||
table.jsonb("Genres").defaultTo(JSON.stringify([]));
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function (knex) {
|
||||
try {
|
||||
const hasTable = await knex.schema.hasTable("jf_library_items");
|
||||
if (hasTable) {
|
||||
await knex.schema.alterTable("jf_library_items", function (table) {
|
||||
table.dropColumn("Genres"); // Drop the column during rollback
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
@@ -1,17 +1,50 @@
|
||||
|
||||
|
||||
const update_query = [
|
||||
{table:'jf_activity_watchdog',query:' ON CONFLICT ("Id") DO UPDATE SET "TranscodingInfo" = EXCLUDED."TranscodingInfo", "MediaStreams" = EXCLUDED."MediaStreams", "PlayMethod" = EXCLUDED."PlayMethod","ActivityDateInserted" = EXCLUDED."ActivityDateInserted","PlaybackDuration" = EXCLUDED."PlaybackDuration","IsPaused"= EXCLUDED."IsPaused"'},
|
||||
{table:'jf_item_info',query:' ON CONFLICT ("Id") DO UPDATE SET "Path" = EXCLUDED."Path", "Name" = EXCLUDED."Name", "Size" = EXCLUDED."Size", "Bitrate" = EXCLUDED."Bitrate", "MediaStreams" = EXCLUDED."MediaStreams"'},
|
||||
{table:'jf_libraries',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "Type" = EXCLUDED."Type", "CollectionType" = EXCLUDED."CollectionType", "ImageTagsPrimary" = EXCLUDED."ImageTagsPrimary", archived=false'},
|
||||
{table:'jf_library_episodes',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PremiereDate" = EXCLUDED."PremiereDate", "OfficialRating" = EXCLUDED."OfficialRating", "CommunityRating" = EXCLUDED."CommunityRating", "RunTimeTicks" = EXCLUDED."RunTimeTicks", "ProductionYear" = EXCLUDED."ProductionYear", "IndexNumber" = EXCLUDED."IndexNumber", "ParentIndexNumber" = EXCLUDED."ParentIndexNumber", "Type" = EXCLUDED."Type", "ParentLogoItemId" = EXCLUDED."ParentLogoItemId", "ParentBackdropItemId" = EXCLUDED."ParentBackdropItemId", "ParentBackdropImageTags" = EXCLUDED."ParentBackdropImageTags", "SeriesId" = EXCLUDED."SeriesId", "SeasonId" = EXCLUDED."SeasonId", "SeasonName" = EXCLUDED."SeasonName", "SeriesName" = EXCLUDED."SeriesName", "PrimaryImageHash" = EXCLUDED."PrimaryImageHash", "DateCreated" = EXCLUDED."DateCreated" , archived=false'},
|
||||
{table:'jf_library_items',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PremiereDate" = EXCLUDED."PremiereDate", "EndDate" = EXCLUDED."EndDate", "CommunityRating" = EXCLUDED."CommunityRating", "RunTimeTicks" = EXCLUDED."RunTimeTicks", "ProductionYear" = EXCLUDED."ProductionYear", "Type" = EXCLUDED."Type", "Status" = EXCLUDED."Status", "ImageTagsPrimary" = EXCLUDED."ImageTagsPrimary", "ImageTagsBanner" = EXCLUDED."ImageTagsBanner", "ImageTagsLogo" = EXCLUDED."ImageTagsLogo", "ImageTagsThumb" = EXCLUDED."ImageTagsThumb", "BackdropImageTags" = EXCLUDED."BackdropImageTags", "ParentId" = EXCLUDED."ParentId", "PrimaryImageHash" = EXCLUDED."PrimaryImageHash", "DateCreated" = EXCLUDED."DateCreated", archived=false'},
|
||||
{table:'jf_library_seasons',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "ParentLogoItemId" = EXCLUDED."ParentLogoItemId", "ParentBackdropItemId" = EXCLUDED."ParentBackdropItemId", "ParentBackdropImageTags" = EXCLUDED."ParentBackdropImageTags", "SeriesPrimaryImageTag" = EXCLUDED."SeriesPrimaryImageTag" , archived=false'},
|
||||
{table:'jf_logging',query:` ON CONFLICT ("Id") DO UPDATE SET "Duration" = EXCLUDED."Duration", "Log"=EXCLUDED."Log", "Result"=EXCLUDED."Result" WHERE "jf_logging"."Result"='Running'`},
|
||||
{table:'jf_playback_activity',query:' ON CONFLICT ("Id") DO UPDATE SET "PlaybackDuration" = EXCLUDED."PlaybackDuration", "ActivityDateInserted" = EXCLUDED."ActivityDateInserted"'},
|
||||
{table:'jf_playback_reporting_plugin_data',query:' ON CONFLICT DO NOTHING'},
|
||||
{table:'jf_users',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PrimaryImageTag" = EXCLUDED."PrimaryImageTag", "LastLoginDate" = EXCLUDED."LastLoginDate", "LastActivityDate" = EXCLUDED."LastActivityDate"'}
|
||||
];
|
||||
module.exports = {
|
||||
update_query
|
||||
};
|
||||
const update_query = [
|
||||
{
|
||||
table: "jf_activity_watchdog",
|
||||
query:
|
||||
' ON CONFLICT ("Id") DO UPDATE SET "TranscodingInfo" = EXCLUDED."TranscodingInfo", "MediaStreams" = EXCLUDED."MediaStreams", "PlayMethod" = EXCLUDED."PlayMethod","ActivityDateInserted" = EXCLUDED."ActivityDateInserted","PlaybackDuration" = EXCLUDED."PlaybackDuration","IsPaused"= EXCLUDED."IsPaused"',
|
||||
},
|
||||
{
|
||||
table: "jf_item_info",
|
||||
query:
|
||||
' ON CONFLICT ("Id") DO UPDATE SET "Path" = EXCLUDED."Path", "Name" = EXCLUDED."Name", "Size" = EXCLUDED."Size", "Bitrate" = EXCLUDED."Bitrate", "MediaStreams" = EXCLUDED."MediaStreams"',
|
||||
},
|
||||
{
|
||||
table: "jf_libraries",
|
||||
query:
|
||||
' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "Type" = EXCLUDED."Type", "CollectionType" = EXCLUDED."CollectionType", "ImageTagsPrimary" = EXCLUDED."ImageTagsPrimary", archived=false',
|
||||
},
|
||||
{
|
||||
table: "jf_library_episodes",
|
||||
query:
|
||||
' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PremiereDate" = EXCLUDED."PremiereDate", "OfficialRating" = EXCLUDED."OfficialRating", "CommunityRating" = EXCLUDED."CommunityRating", "RunTimeTicks" = EXCLUDED."RunTimeTicks", "ProductionYear" = EXCLUDED."ProductionYear", "IndexNumber" = EXCLUDED."IndexNumber", "ParentIndexNumber" = EXCLUDED."ParentIndexNumber", "Type" = EXCLUDED."Type", "ParentLogoItemId" = EXCLUDED."ParentLogoItemId", "ParentBackdropItemId" = EXCLUDED."ParentBackdropItemId", "ParentBackdropImageTags" = EXCLUDED."ParentBackdropImageTags", "SeriesId" = EXCLUDED."SeriesId", "SeasonId" = EXCLUDED."SeasonId", "SeasonName" = EXCLUDED."SeasonName", "SeriesName" = EXCLUDED."SeriesName", "PrimaryImageHash" = EXCLUDED."PrimaryImageHash", "DateCreated" = EXCLUDED."DateCreated" , archived=false',
|
||||
},
|
||||
{
|
||||
table: "jf_library_items",
|
||||
query:
|
||||
' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PremiereDate" = EXCLUDED."PremiereDate", "EndDate" = EXCLUDED."EndDate", "CommunityRating" = EXCLUDED."CommunityRating", "RunTimeTicks" = EXCLUDED."RunTimeTicks", "ProductionYear" = EXCLUDED."ProductionYear", "Type" = EXCLUDED."Type", "Status" = EXCLUDED."Status", "ImageTagsPrimary" = EXCLUDED."ImageTagsPrimary", "ImageTagsBanner" = EXCLUDED."ImageTagsBanner", "ImageTagsLogo" = EXCLUDED."ImageTagsLogo", "ImageTagsThumb" = EXCLUDED."ImageTagsThumb", "BackdropImageTags" = EXCLUDED."BackdropImageTags", "ParentId" = EXCLUDED."ParentId", "PrimaryImageHash" = EXCLUDED."PrimaryImageHash", "DateCreated" = EXCLUDED."DateCreated", archived=false, "Genres" = EXCLUDED."Genres"',
|
||||
},
|
||||
{
|
||||
table: "jf_library_seasons",
|
||||
query:
|
||||
' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "ParentLogoItemId" = EXCLUDED."ParentLogoItemId", "ParentBackdropItemId" = EXCLUDED."ParentBackdropItemId", "ParentBackdropImageTags" = EXCLUDED."ParentBackdropImageTags", "SeriesPrimaryImageTag" = EXCLUDED."SeriesPrimaryImageTag" , archived=false',
|
||||
},
|
||||
{
|
||||
table: "jf_logging",
|
||||
query: ` ON CONFLICT ("Id") DO UPDATE SET "Duration" = EXCLUDED."Duration", "Log"=EXCLUDED."Log", "Result"=EXCLUDED."Result" WHERE "jf_logging"."Result"='Running'`,
|
||||
},
|
||||
{
|
||||
table: "jf_playback_activity",
|
||||
query:
|
||||
' ON CONFLICT ("Id") DO UPDATE SET "PlaybackDuration" = EXCLUDED."PlaybackDuration", "ActivityDateInserted" = EXCLUDED."ActivityDateInserted"',
|
||||
},
|
||||
{ table: "jf_playback_reporting_plugin_data", query: " ON CONFLICT DO NOTHING" },
|
||||
{
|
||||
table: "jf_users",
|
||||
query:
|
||||
' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PrimaryImageTag" = EXCLUDED."PrimaryImageTag", "LastLoginDate" = EXCLUDED."LastLoginDate", "LastActivityDate" = EXCLUDED."LastActivityDate"',
|
||||
},
|
||||
];
|
||||
module.exports = {
|
||||
update_query,
|
||||
};
|
||||
|
||||
@@ -1,55 +1,59 @@
|
||||
const jf_library_items_columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"PremiereDate",
|
||||
"DateCreated",
|
||||
"EndDate",
|
||||
"CommunityRating",
|
||||
"RunTimeTicks",
|
||||
"ProductionYear",
|
||||
"IsFolder",
|
||||
"Type",
|
||||
"Status",
|
||||
"ImageTagsPrimary",
|
||||
"ImageTagsBanner",
|
||||
"ImageTagsLogo",
|
||||
"ImageTagsThumb",
|
||||
"BackdropImageTags",
|
||||
"ParentId",
|
||||
"PrimaryImageHash",
|
||||
"archived",
|
||||
"Genres",
|
||||
];
|
||||
|
||||
const jf_library_items_columns = [
|
||||
"Id",
|
||||
"Name",
|
||||
"ServerId",
|
||||
"PremiereDate",
|
||||
"DateCreated",
|
||||
"EndDate",
|
||||
"CommunityRating",
|
||||
"RunTimeTicks",
|
||||
"ProductionYear",
|
||||
"IsFolder",
|
||||
"Type",
|
||||
"Status",
|
||||
"ImageTagsPrimary",
|
||||
"ImageTagsBanner",
|
||||
"ImageTagsLogo",
|
||||
"ImageTagsThumb",
|
||||
"BackdropImageTags",
|
||||
"ParentId",
|
||||
"PrimaryImageHash",
|
||||
"archived",
|
||||
];
|
||||
const jf_library_items_mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
DateCreated: item.DateCreated,
|
||||
EndDate: item.EndDate,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
Status: item.Status,
|
||||
ImageTagsPrimary: item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
ImageTagsBanner: item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null,
|
||||
ImageTagsLogo: item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null,
|
||||
ImageTagsThumb: item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null,
|
||||
BackdropImageTags: item.BackdropImageTags[0],
|
||||
ParentId: item.ParentId,
|
||||
PrimaryImageHash:
|
||||
item.ImageTags &&
|
||||
item.ImageTags.Primary &&
|
||||
item.ImageBlurHashes &&
|
||||
item.ImageBlurHashes.Primary &&
|
||||
item.ImageBlurHashes.Primary[item.ImageTags["Primary"]]
|
||||
? item.ImageBlurHashes.Primary[item.ImageTags["Primary"]]
|
||||
: null,
|
||||
archived: false,
|
||||
Genres: item.Genres && Array.isArray(item.Genres) ? JSON.stringify(item.Genres) : [],
|
||||
});
|
||||
|
||||
const jf_library_items_mapping = (item) => ({
|
||||
Id: item.Id,
|
||||
Name: item.Name,
|
||||
ServerId: item.ServerId,
|
||||
PremiereDate: item.PremiereDate,
|
||||
DateCreated: item.DateCreated,
|
||||
EndDate: item.EndDate,
|
||||
CommunityRating: item.CommunityRating,
|
||||
RunTimeTicks: item.RunTimeTicks,
|
||||
ProductionYear: item.ProductionYear,
|
||||
IsFolder: item.IsFolder,
|
||||
Type: item.Type,
|
||||
Status: item.Status,
|
||||
ImageTagsPrimary:
|
||||
item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
ImageTagsBanner:
|
||||
item.ImageTags && item.ImageTags.Banner ? item.ImageTags.Banner : null,
|
||||
ImageTagsLogo:
|
||||
item.ImageTags && item.ImageTags.Logo ? item.ImageTags.Logo : null,
|
||||
ImageTagsThumb:
|
||||
item.ImageTags && item.ImageTags.Thumb ? item.ImageTags.Thumb : null,
|
||||
BackdropImageTags: item.BackdropImageTags[0],
|
||||
ParentId: item.ParentId,
|
||||
PrimaryImageHash: item.ImageTags && item.ImageTags.Primary && item.ImageBlurHashes && item.ImageBlurHashes.Primary && item.ImageBlurHashes.Primary[item.ImageTags["Primary"]] ? item.ImageBlurHashes.Primary[item.ImageTags["Primary"]] : null,
|
||||
archived: false,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
jf_library_items_columns,
|
||||
jf_library_items_mapping,
|
||||
};
|
||||
module.exports = {
|
||||
jf_library_items_columns,
|
||||
jf_library_items_mapping,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// api.js
|
||||
const express = require("express");
|
||||
const db = require("../db");
|
||||
const dbHelper = require("../classes/db-helper");
|
||||
const moment = require("moment");
|
||||
|
||||
const router = express.Router();
|
||||
@@ -515,6 +516,69 @@ router.post("/getViewsByHour", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/getGenreStats", async (req, res) => {
|
||||
try {
|
||||
const { size = 50, page = 1, userid } = req.query;
|
||||
|
||||
if (userid === undefined) {
|
||||
res.status(400);
|
||||
res.send("No User ID provided");
|
||||
return;
|
||||
}
|
||||
|
||||
const values = [];
|
||||
const query = {
|
||||
select: ["COALESCE(g.genre, 'No Genre') AS genre", `SUM(a."PlaybackDuration") AS duration`, "COUNT(*) AS plays"],
|
||||
table: "jf_playback_activity_with_metadata",
|
||||
alias: "a",
|
||||
joins: [
|
||||
{
|
||||
type: "inner",
|
||||
table: "jf_library_items",
|
||||
alias: "i",
|
||||
conditions: [{ first: "a.NowPlayingItemId", operator: "=", second: "i.Id" }],
|
||||
},
|
||||
{
|
||||
type: "left",
|
||||
table: `
|
||||
LATERAL (
|
||||
SELECT
|
||||
jsonb_array_elements_text(
|
||||
CASE
|
||||
WHEN jsonb_array_length(COALESCE(i."Genres", '[]'::jsonb)) = 0 THEN '["No Genre"]'::jsonb
|
||||
ELSE i."Genres"
|
||||
END
|
||||
) AS genre
|
||||
)
|
||||
`,
|
||||
alias: "g",
|
||||
conditions: [{ first: 1, operator: "=", value: 1, wrap: false }],
|
||||
},
|
||||
],
|
||||
|
||||
where: [[{ column: "a.UserId", operator: "=", value: `$${values.length + 1}` }]],
|
||||
group_by: [`COALESCE(g.genre, 'No Genre')`],
|
||||
order_by: "genre",
|
||||
pageNumber: page,
|
||||
pageSize: size,
|
||||
};
|
||||
|
||||
values.push(userid);
|
||||
|
||||
query.values = values;
|
||||
|
||||
const result = await dbHelper.query(query);
|
||||
|
||||
const response = { current_page: page, pages: result.pages, size: size, results: result.results };
|
||||
|
||||
res.send(response);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
res.status(503);
|
||||
res.send(error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle other routes
|
||||
router.use((req, res) => {
|
||||
res.status(404).send({ error: "Not Found" });
|
||||
|
||||
@@ -313,5 +313,6 @@
|
||||
"POSTCODE": "Codi postal",
|
||||
"X_ROWS_SELECTED": "{ROWS} files seleccionades",
|
||||
"TRANSCODE_REASONS": "",
|
||||
"SUBTITLES": "Subtítols"
|
||||
"SUBTITLES": "Subtítols",
|
||||
"GENRES": "Gèneres"
|
||||
}
|
||||
|
||||
@@ -314,5 +314,6 @@
|
||||
"POSTCODE": "Postcode",
|
||||
"X_ROWS_SELECTED": "{ROWS} Rows Selected",
|
||||
"TRANSCODE_REASONS": "Transcode Reasons",
|
||||
"SUBTITLES": "Subtitles"
|
||||
"SUBTITLES": "Subtitles",
|
||||
"GENRES": "Genres"
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@
|
||||
"TIMEZONE": "Fuseau horaire",
|
||||
"POSTCODE": "Code postal",
|
||||
"X_ROWS_SELECTED": "{ROWS} Ligne(s) sélectionnée(s)",
|
||||
"TRANSCODE_REASONS": "",
|
||||
"SUBTITLES": ""
|
||||
"TRANSCODE_REASONS": "Raisons du transcodage",
|
||||
"SUBTITLES": "Sous-titres",
|
||||
"GENRES": "Genres"
|
||||
}
|
||||
|
||||
@@ -309,6 +309,7 @@
|
||||
"TIMEZONE": "Fuso orario",
|
||||
"POSTCODE": "CAP",
|
||||
"X_ROWS_SELECTED": "{ROWS} righe selezionate",
|
||||
"TRANSCODE_REASONS": "",
|
||||
"SUBTITLES": ""
|
||||
"TRANSCODE_REASONS": "Motivo della transcodifica",
|
||||
"SUBTITLES": "Sottotitoli",
|
||||
"GENRES": "Generi"
|
||||
}
|
||||
|
||||
@@ -313,5 +313,6 @@
|
||||
"POSTCODE": "邮编",
|
||||
"X_ROWS_SELECTED": "已选中 {ROWS} 行",
|
||||
"TRANSCODE_REASONS": "转码原因",
|
||||
"SUBTITLES": "字幕"
|
||||
"SUBTITLES": "字幕",
|
||||
"GENRES": "类型"
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Trans } from "react-i18next";
|
||||
import baseUrl from "../../lib/baseurl";
|
||||
import GlobalStats from "./general/globalStats";
|
||||
import ActivityTimeline from "../activity_time_line";
|
||||
import GenreStats from "./user-info/genre-stats.jsx";
|
||||
|
||||
function UserInfo() {
|
||||
const { UserId } = useParams();
|
||||
@@ -78,12 +79,7 @@ function UserInfo() {
|
||||
) : (
|
||||
<img
|
||||
className="user-image"
|
||||
src={
|
||||
baseUrl +
|
||||
"/proxy/Users/Images/Primary?id=" +
|
||||
UserId +
|
||||
"&quality=100"
|
||||
}
|
||||
src={baseUrl + "/proxy/Users/Images/Primary?id=" + UserId + "&quality=100"}
|
||||
onError={handleImageError}
|
||||
alt=""
|
||||
></img>
|
||||
@@ -121,11 +117,7 @@ function UserInfo() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
defaultActiveKey="tabOverview"
|
||||
activeKey={activeTab}
|
||||
variant="pills"
|
||||
>
|
||||
<Tabs defaultActiveKey="tabOverview" activeKey={activeTab} variant="pills">
|
||||
<Tab eventKey="tabOverview" className="bg-transparent">
|
||||
<GlobalStats
|
||||
id={UserId}
|
||||
@@ -133,6 +125,7 @@ function UserInfo() {
|
||||
endpoint={"getGlobalUserStats"}
|
||||
title={<Trans i18nKey="USERS_PAGE.USER_STATS" />}
|
||||
/>
|
||||
<GenreStats UserId={UserId} />
|
||||
<LastPlayed UserId={UserId} />
|
||||
</Tab>
|
||||
<Tab eventKey="tabActivity" className="bg-transparent">
|
||||
|
||||
87
src/pages/components/user-info/genre-stat-card.jsx
Normal file
87
src/pages/components/user-info/genre-stat-card.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import { Radar, RadarChart, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ResponsiveContainer, Tooltip } from "recharts";
|
||||
|
||||
import ErrorBoundary from "../general/ErrorBoundary.jsx";
|
||||
|
||||
import "../../css/genres.css";
|
||||
import { Trans } from "react-i18next";
|
||||
import i18next from "i18next";
|
||||
|
||||
function GenreStatCard(props) {
|
||||
const [maxRange, setMaxRange] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
const maxDuration = props.data.reduce((max, item) => {
|
||||
return Math.max(max, parseFloat((props.dataKey == "duration" ? item.duration : item.plays) || 0));
|
||||
}, 0);
|
||||
setMaxRange(maxDuration);
|
||||
}, [props.data, props.dataKey]);
|
||||
|
||||
const CustomTooltip = ({ active, payload }) => {
|
||||
if (active && payload && payload.length) {
|
||||
return (
|
||||
<div className="radial-tooltip">
|
||||
<p className="tooltip-header">{payload[0].payload.genre}</p>
|
||||
<p>
|
||||
{props.dataKey == "duration" ? formatTotalWatchTime(payload[0].value) : payload[0].value} {i18next.t("UNITS.PLAYS")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
function formatTotalWatchTime(seconds) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
|
||||
let timeString = "";
|
||||
|
||||
if (hours > 0) {
|
||||
timeString += `${hours} ${hours === 1 ? i18next.t("UNITS.HOUR").toLowerCase() : i18next.t("UNITS.HOURS").toLowerCase()} `;
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
timeString += `${minutes} ${
|
||||
minutes === 1 ? i18next.t("UNITS.MINUTE").toLowerCase() : i18next.t("UNITS.MINUTES").toLowerCase()
|
||||
} `;
|
||||
}
|
||||
|
||||
if (remainingSeconds > 0) {
|
||||
timeString += `${remainingSeconds} ${
|
||||
remainingSeconds === 1 ? i18next.t("UNITS.SECOND").toLowerCase() : i18next.t("UNITS.SECONDS").toLowerCase()
|
||||
}`;
|
||||
}
|
||||
|
||||
return timeString.trim();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="genre-stats">
|
||||
<h1 className="my-3 text-center">
|
||||
<Trans i18nKey={props.dataKey == "duration" ? "SETTINGS_PAGE.DURATION" : "TOTAL_PLAYS"} />
|
||||
</h1>
|
||||
<ErrorBoundary>
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<RadarChart cx="50%" cy="50%" outerRadius="80%" data={props.data}>
|
||||
<PolarGrid gridType="circle" />
|
||||
<PolarAngleAxis dataKey="genre" />
|
||||
<PolarRadiusAxis domain={[0, maxRange]} tick={false} />
|
||||
<Radar
|
||||
name="Duration"
|
||||
dataKey={props.dataKey}
|
||||
stroke={`var(--tertiary-background-color)`}
|
||||
fill={`var(--secondary-color)`}
|
||||
fillOpacity={0.6}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
</RadarChart>
|
||||
</ResponsiveContainer>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenreStatCard;
|
||||
77
src/pages/components/user-info/genre-stats.jsx
Normal file
77
src/pages/components/user-info/genre-stats.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Row, Col } from "react-bootstrap";
|
||||
import GenreStatCard from "./genre-stat-card.jsx";
|
||||
import { Trans } from "react-i18next";
|
||||
import "../../css/genres.css";
|
||||
import Config from "../../../lib/config";
|
||||
import axios from "../../../lib/axios_instance";
|
||||
|
||||
function GenreStats(props) {
|
||||
const [data, setData] = useState();
|
||||
const [config, setConfig] = useState();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
const newConfig = await Config.getConfig();
|
||||
setConfig(newConfig);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
if (config) {
|
||||
try {
|
||||
const itemData = await axios.get(`/stats/getGenreStats`, {
|
||||
params: {
|
||||
userid: props.UserId,
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const results = itemData.data.results || [];
|
||||
setData(results);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!data) {
|
||||
fetchData();
|
||||
}
|
||||
|
||||
if (!config) {
|
||||
fetchConfig();
|
||||
}
|
||||
|
||||
const intervalId = setInterval(fetchData, 60000 * 5);
|
||||
return () => clearInterval(intervalId);
|
||||
}, [data, config, props.UserId]);
|
||||
|
||||
if (!data || data.length == 0 || !config) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<div className="genre">
|
||||
<h1 className="my-3">
|
||||
<Trans i18nKey="GENRES" />
|
||||
</h1>
|
||||
<div className="genre-container d-flex flex-row justify-content-between">
|
||||
<Row className="w-100 pb-5">
|
||||
<Col className="p-0 auto">
|
||||
<GenreStatCard data={data} dataKey="duration" />
|
||||
</Col>
|
||||
<Col className="p-0 auto">
|
||||
<GenreStatCard data={data} dataKey="plays" />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GenreStats;
|
||||
50
src/pages/css/genres.css
Normal file
50
src/pages/css/genres.css
Normal file
@@ -0,0 +1,50 @@
|
||||
@import "./variables.module.css";
|
||||
.genre-container {
|
||||
display: flex;
|
||||
overflow-x: none;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.radial-tooltip {
|
||||
background-color: black;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.radial-tooltip > p {
|
||||
padding: 0px !important;
|
||||
margin: 0px !important;
|
||||
}
|
||||
|
||||
.radial-tooltip > .tooltip-header {
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.genre-stats {
|
||||
width: 90%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.genre-container::-webkit-scrollbar {
|
||||
width: 5px; /* set scrollbar width */
|
||||
}
|
||||
|
||||
.genre-container::-webkit-scrollbar-track {
|
||||
background-color: transparent; /* set track color */
|
||||
}
|
||||
|
||||
.genre-container::-webkit-scrollbar-thumb {
|
||||
background-color: #8888884d; /* set thumb color */
|
||||
border-radius: 5px; /* round corners */
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
.genre-container::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #88888883; /* set thumb color */
|
||||
}
|
||||
@@ -1,38 +1,31 @@
|
||||
@import '../variables.module.css';
|
||||
@import "../variables.module.css";
|
||||
|
||||
.user-detail-container
|
||||
{
|
||||
color:white;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
.user-detail-container {
|
||||
color: white;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-name
|
||||
{
|
||||
font-size: 2.5em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
.user-name {
|
||||
font-size: 2.5em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
}
|
||||
.user-image
|
||||
{
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 0 10px 5px var(--secondary-background-color);
|
||||
.user-image {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 0 10px 5px var(--secondary-background-color);
|
||||
}
|
||||
|
||||
.user-image-container
|
||||
{
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 20px;
|
||||
|
||||
|
||||
}
|
||||
.user-image-container {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
@@ -196,9 +196,15 @@ function Row(row) {
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell style={{ textTransform: data.LastWatched ? "none" : "lowercase" }}>
|
||||
<Link to={`/libraries/item/${data.NowPlayingItemId}`} className="text-decoration-none">
|
||||
{data.LastWatched || i18next.t("ERROR_MESSAGES.NEVER")}
|
||||
</Link>
|
||||
{data.NowPlayingItemId ? (
|
||||
<Link to={`/libraries/item/${data.NowPlayingItemId}`} className="text-decoration-none">
|
||||
{data.LastWatched || i18next.t("ERROR_MESSAGES.NEVER")}
|
||||
</Link>
|
||||
) : (
|
||||
<span to={`/libraries/item/${data.NowPlayingItemId}`} className="text-decoration-none">
|
||||
{data.LastWatched || i18next.t("ERROR_MESSAGES.NEVER")}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell style={{ textTransform: data.LastClient ? "none" : "lowercase" }}>
|
||||
{data.LastClient || i18next.t("ERROR_MESSAGES.N/A")}
|
||||
|
||||
Reference in New Issue
Block a user