mirror of
https://github.com/BreizhHardware/Jellystat.git
synced 2026-03-18 21:30:35 +01:00
added archiving feature to libraries
added archiving flag to libraries. If a library is deleted or excluded the entire library and its contents are archived when the sync process is run Added archive toggle in libraries to show/hide archived libraries Still need to add functionality to bulk purge archived data for a library or for selected items in the library. cleaned up duplicate statement in migration 46 uncommented backwards navigation code when purge completes
This commit is contained in:
@@ -7,49 +7,6 @@ exports.up = async function(knex) {
|
||||
table.boolean('archived').defaultTo(false);
|
||||
|
||||
});
|
||||
|
||||
await knex.schema.raw(`
|
||||
DROP FUNCTION IF EXISTS public.fs_last_library_activity(text);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fs_last_library_activity(
|
||||
libraryid text)
|
||||
RETURNS TABLE("Id" text, "EpisodeId" text, "Name" text, "EpisodeName" text, "SeasonNumber" integer, "EpisodeNumber" integer, "PrimaryImageHash" text, "UserId" text, "UserName" text, archived boolean, "LastPlayed" interval)
|
||||
LANGUAGE 'plpgsql'
|
||||
COST 100
|
||||
VOLATILE PARALLEL UNSAFE
|
||||
ROWS 1000
|
||||
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT DISTINCT ON (i."Name", e."Name")
|
||||
i."Id",
|
||||
a."EpisodeId",
|
||||
i."Name",
|
||||
e."Name" AS "EpisodeName",
|
||||
CASE WHEN a."SeasonId" IS NOT NULL THEN s."IndexNumber" ELSE NULL END AS "SeasonNumber",
|
||||
CASE WHEN a."SeasonId" IS NOT NULL THEN e."IndexNumber" ELSE NULL END AS "EpisodeNumber",
|
||||
i."PrimaryImageHash",
|
||||
a."UserId",
|
||||
a."UserName",
|
||||
i.archived,
|
||||
(NOW() - a."ActivityDateInserted") as "LastPlayed"
|
||||
FROM jf_playback_activity a
|
||||
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
|
||||
JOIN jf_libraries l ON i."ParentId" = l."Id"
|
||||
LEFT JOIN jf_library_seasons s ON s."Id" = a."SeasonId"
|
||||
LEFT JOIN jf_library_episodes e ON e."EpisodeId" = a."EpisodeId"
|
||||
WHERE l."Id" = libraryid
|
||||
ORDER BY i."Name", e."Name", a."ActivityDateInserted" DESC
|
||||
) AS latest_distinct_rows
|
||||
ORDER BY "LastPlayed"
|
||||
LIMIT 15;
|
||||
END;
|
||||
|
||||
|
||||
$BODY$;`);
|
||||
}
|
||||
}catch (error) {
|
||||
console.error(error);
|
||||
@@ -62,47 +19,6 @@ exports.down = async function(knex) {
|
||||
table.dropColumn('archived');
|
||||
});
|
||||
|
||||
await knex.schema.raw(`
|
||||
DROP FUNCTION IF EXISTS public.fs_last_library_activity(text);
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fs_last_library_activity(
|
||||
libraryid text)
|
||||
RETURNS TABLE("Id" text, "EpisodeId" text, "Name" text, "EpisodeName" text, "SeasonNumber" integer, "EpisodeNumber" integer, "PrimaryImageHash" text, "UserId" text, "UserName" text, "LastPlayed" interval)
|
||||
LANGUAGE 'plpgsql'
|
||||
COST 100
|
||||
VOLATILE PARALLEL UNSAFE
|
||||
ROWS 1000
|
||||
|
||||
AS $BODY$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
SELECT *
|
||||
FROM (
|
||||
SELECT DISTINCT ON (i."Name", e."Name")
|
||||
i."Id",
|
||||
a."EpisodeId",
|
||||
i."Name",
|
||||
e."Name" AS "EpisodeName",
|
||||
CASE WHEN a."SeasonId" IS NOT NULL THEN s."IndexNumber" ELSE NULL END AS "SeasonNumber",
|
||||
CASE WHEN a."SeasonId" IS NOT NULL THEN e."IndexNumber" ELSE NULL END AS "EpisodeNumber",
|
||||
i."PrimaryImageHash",
|
||||
a."UserId",
|
||||
a."UserName",
|
||||
(NOW() - a."ActivityDateInserted") as "LastPlayed"
|
||||
FROM jf_playback_activity a
|
||||
JOIN jf_library_items i ON i."Id" = a."NowPlayingItemId"
|
||||
JOIN jf_libraries l ON i."ParentId" = l."Id"
|
||||
LEFT JOIN jf_library_seasons s ON s."Id" = a."SeasonId"
|
||||
LEFT JOIN jf_library_episodes e ON e."EpisodeId" = a."EpisodeId"
|
||||
WHERE l."Id" = libraryid
|
||||
ORDER BY i."Name", e."Name", a."ActivityDateInserted" DESC
|
||||
) AS latest_distinct_rows
|
||||
ORDER BY "LastPlayed"
|
||||
LIMIT 15;
|
||||
END;
|
||||
|
||||
|
||||
$BODY$;`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
exports.up = async function(knex) {
|
||||
try
|
||||
{
|
||||
const hasTable = await knex.schema.hasTable('jf_libraries');
|
||||
if (hasTable) {
|
||||
await knex.schema.alterTable('jf_libraries', function(table) {
|
||||
table.boolean('archived').defaultTo(false);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
}catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
try {
|
||||
await knex.schema.alterTable('jf_libraries', function(table) {
|
||||
table.dropColumn('archived');
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
132
backend/migrations/053_js_library_stats_overview.js
Normal file
132
backend/migrations/053_js_library_stats_overview.js
Normal file
@@ -0,0 +1,132 @@
|
||||
exports.up = async function(knex) {
|
||||
try
|
||||
{
|
||||
await knex.schema.raw(`
|
||||
DROP VIEW public.js_library_stats_overview;
|
||||
|
||||
CREATE OR REPLACE VIEW public.js_library_stats_overview
|
||||
AS
|
||||
SELECT DISTINCT ON (l."Id") l."Id",
|
||||
l."Name",
|
||||
l."ServerId",
|
||||
l."IsFolder",
|
||||
l."Type",
|
||||
l."CollectionType",
|
||||
l."ImageTagsPrimary",
|
||||
i."Id" AS "ItemId",
|
||||
i."Name" AS "ItemName",
|
||||
i."Type" AS "ItemType",
|
||||
i."PrimaryImageHash",
|
||||
s."IndexNumber" AS "SeasonNumber",
|
||||
e."IndexNumber" AS "EpisodeNumber",
|
||||
e."Name" AS "EpisodeName",
|
||||
( SELECT count(*) AS count
|
||||
FROM jf_playback_activity a
|
||||
JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
|
||||
WHERE i_1."ParentId" = l."Id") AS "Plays",
|
||||
( SELECT sum(a."PlaybackDuration") AS sum
|
||||
FROM jf_playback_activity a
|
||||
JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
|
||||
WHERE i_1."ParentId" = l."Id") AS total_playback_duration,
|
||||
l.total_play_time::numeric AS total_play_time,
|
||||
l.item_count AS "Library_Count",
|
||||
l.season_count AS "Season_Count",
|
||||
l.episode_count AS "Episode_Count",
|
||||
l.archived,
|
||||
now() - latest_activity."ActivityDateInserted" AS "LastActivity"
|
||||
|
||||
FROM jf_libraries l
|
||||
LEFT JOIN ( SELECT DISTINCT ON (i_1."ParentId") jf_playback_activity."Id",
|
||||
jf_playback_activity."IsPaused",
|
||||
jf_playback_activity."UserId",
|
||||
jf_playback_activity."UserName",
|
||||
jf_playback_activity."Client",
|
||||
jf_playback_activity."DeviceName",
|
||||
jf_playback_activity."DeviceId",
|
||||
jf_playback_activity."ApplicationVersion",
|
||||
jf_playback_activity."NowPlayingItemId",
|
||||
jf_playback_activity."NowPlayingItemName",
|
||||
jf_playback_activity."SeasonId",
|
||||
jf_playback_activity."SeriesName",
|
||||
jf_playback_activity."EpisodeId",
|
||||
jf_playback_activity."PlaybackDuration",
|
||||
jf_playback_activity."ActivityDateInserted",
|
||||
jf_playback_activity."PlayMethod",
|
||||
i_1."ParentId"
|
||||
FROM jf_playback_activity
|
||||
JOIN jf_library_items i_1 ON i_1."Id" = jf_playback_activity."NowPlayingItemId"
|
||||
ORDER BY i_1."ParentId", jf_playback_activity."ActivityDateInserted" DESC) latest_activity ON l."Id" = latest_activity."ParentId"
|
||||
LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
|
||||
LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
|
||||
LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
|
||||
ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;`);
|
||||
|
||||
}catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
exports.down = async function(knex) {
|
||||
try {
|
||||
await knex.schema.raw(`
|
||||
DROP VIEW public.js_library_stats_overview;
|
||||
|
||||
CREATE OR REPLACE VIEW public.js_library_stats_overview
|
||||
AS
|
||||
SELECT DISTINCT ON (l."Id") l."Id",
|
||||
l."Name",
|
||||
l."ServerId",
|
||||
l."IsFolder",
|
||||
l."Type",
|
||||
l."CollectionType",
|
||||
l."ImageTagsPrimary",
|
||||
i."Id" AS "ItemId",
|
||||
i."Name" AS "ItemName",
|
||||
i."Type" AS "ItemType",
|
||||
i."PrimaryImageHash",
|
||||
s."IndexNumber" AS "SeasonNumber",
|
||||
e."IndexNumber" AS "EpisodeNumber",
|
||||
e."Name" AS "EpisodeName",
|
||||
( SELECT count(*) AS count
|
||||
FROM jf_playback_activity a
|
||||
JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
|
||||
WHERE i_1."ParentId" = l."Id") AS "Plays",
|
||||
( SELECT sum(a."PlaybackDuration") AS sum
|
||||
FROM jf_playback_activity a
|
||||
JOIN jf_library_items i_1 ON a."NowPlayingItemId" = i_1."Id"
|
||||
WHERE i_1."ParentId" = l."Id") AS total_playback_duration,
|
||||
l.total_play_time::numeric AS total_play_time,
|
||||
l.item_count AS "Library_Count",
|
||||
l.season_count AS "Season_Count",
|
||||
l.episode_count AS "Episode_Count",
|
||||
now() - latest_activity."ActivityDateInserted" AS "LastActivity"
|
||||
|
||||
FROM jf_libraries l
|
||||
LEFT JOIN ( SELECT DISTINCT ON (i_1."ParentId") jf_playback_activity."Id",
|
||||
jf_playback_activity."IsPaused",
|
||||
jf_playback_activity."UserId",
|
||||
jf_playback_activity."UserName",
|
||||
jf_playback_activity."Client",
|
||||
jf_playback_activity."DeviceName",
|
||||
jf_playback_activity."DeviceId",
|
||||
jf_playback_activity."ApplicationVersion",
|
||||
jf_playback_activity."NowPlayingItemId",
|
||||
jf_playback_activity."NowPlayingItemName",
|
||||
jf_playback_activity."SeasonId",
|
||||
jf_playback_activity."SeriesName",
|
||||
jf_playback_activity."EpisodeId",
|
||||
jf_playback_activity."PlaybackDuration",
|
||||
jf_playback_activity."ActivityDateInserted",
|
||||
jf_playback_activity."PlayMethod",
|
||||
i_1."ParentId"
|
||||
FROM jf_playback_activity
|
||||
JOIN jf_library_items i_1 ON i_1."Id" = jf_playback_activity."NowPlayingItemId"
|
||||
ORDER BY i_1."ParentId", jf_playback_activity."ActivityDateInserted" DESC) latest_activity ON l."Id" = latest_activity."ParentId"
|
||||
LEFT JOIN jf_library_items i ON i."Id" = latest_activity."NowPlayingItemId"
|
||||
LEFT JOIN jf_library_seasons s ON s."Id" = latest_activity."SeasonId"
|
||||
LEFT JOIN jf_library_episodes e ON e."EpisodeId" = latest_activity."EpisodeId"
|
||||
ORDER BY l."Id", latest_activity."ActivityDateInserted" DESC;`);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
@@ -7,6 +7,7 @@
|
||||
"Type",
|
||||
"CollectionType",
|
||||
"ImageTagsPrimary",
|
||||
"archived",
|
||||
];
|
||||
|
||||
const jf_libraries_mapping = (item) => ({
|
||||
@@ -18,6 +19,7 @@
|
||||
CollectionType: item.CollectionType? item.CollectionType : 'mixed',
|
||||
ImageTagsPrimary:
|
||||
item.ImageTags && item.ImageTags.Primary ? item.ImageTags.Primary : null,
|
||||
archived: false,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -372,24 +372,22 @@ async function syncLibraryFolders(data)
|
||||
await _sync.insertData("jf_libraries",dataToInsert,jf_libraries_columns);
|
||||
}
|
||||
|
||||
//----------------------DELETE FUNCTION
|
||||
//GET EPISODES IN SEASONS
|
||||
//GET SEASONS IN SHOWS
|
||||
//GET SHOWS IN LIBRARY
|
||||
//FINALY DELETE LIBRARY
|
||||
const toDeleteIds = existingIds.filter((id) =>!data.some((row) => row.Id === id ));
|
||||
if (toDeleteIds.length > 0) {
|
||||
sendUpdate(syncTask.wsKey,{type:"Update",message:"Cleaning Up Old Library Data"});
|
||||
//archive libraries and items instead of deleting them
|
||||
|
||||
const ItemsToDelete=await db.query(`SELECT "Id" FROM jf_library_items where "ParentId" in (${toDeleteIds.map(id => `'${id}'`).join(',')})`).then((res) => res.rows.map((row) => row.Id));
|
||||
if (ItemsToDelete.length > 0) {
|
||||
await _sync.removeData("jf_library_items",ItemsToDelete);
|
||||
const toArchiveLibraryIds = existingIds.filter((id) =>!data.some((row) => row.Id === id ));
|
||||
if (toArchiveLibraryIds.length > 0) {
|
||||
sendUpdate(syncTask.wsKey,{type:"Update",message:"Archiving old Library Data"});
|
||||
|
||||
const ItemsToArchive=await db.query(`SELECT "Id" FROM jf_library_items where "ParentId" in (${toArchiveLibraryIds.map(id => `'${id}'`).join(',')})`).then((res) => res.rows.map((row) => row.Id));
|
||||
if (ItemsToArchive.length > 0) {
|
||||
await _sync.updateSingleFieldOnDB("jf_library_items",ItemsToArchive,"archived",true);
|
||||
}
|
||||
|
||||
await _sync.removeData("jf_libraries",toDeleteIds);
|
||||
await _sync.updateSingleFieldOnDB("jf_libraries",toArchiveLibraryIds,"archived",true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
async function syncLibraryItems(data)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ function ItemOptions(props) {
|
||||
}).then((response) => {
|
||||
console.log(response);
|
||||
setShow(false);
|
||||
// navigate(-1);
|
||||
navigate(-1);
|
||||
}).catch((error) => {
|
||||
console.log({error:error,token:token});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import axios from "axios";
|
||||
import Config from "../lib/config";
|
||||
|
||||
@@ -6,12 +6,16 @@ import "./css/library/libraries.css";
|
||||
import Loading from "./components/general/loading";
|
||||
import LibraryCard from "./components/library/library-card";
|
||||
import ErrorBoundary from "./components/general/ErrorBoundary";
|
||||
import EyeOffFillIcon from 'remixicon-react/EyeOffFillIcon';
|
||||
import EyeFillIcon from 'remixicon-react/EyeFillIcon';
|
||||
import { Tooltip } from "react-bootstrap";
|
||||
|
||||
|
||||
function Libraries() {
|
||||
const [data, setData] = useState();
|
||||
const [metadata, setMetaData] = useState();
|
||||
const [config, setConfig] = useState(null);
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchConfig = async () => {
|
||||
@@ -77,11 +81,28 @@ function Libraries() {
|
||||
|
||||
return (
|
||||
<div className="libraries">
|
||||
<div className="d-flex flex-row justify-content-between">
|
||||
<h1 className="py-4">Libraries</h1>
|
||||
{
|
||||
showArchived ?
|
||||
<Tooltip title={"Hide Archived Libraries"}>
|
||||
<button className="btn" onClick={()=> setShowArchived(!showArchived)}>
|
||||
<EyeFillIcon/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip title={"Show Archived Libraries"}>
|
||||
<button className="btn" onClick={()=> setShowArchived(!showArchived)}>
|
||||
<EyeOffFillIcon/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
<div xs={1} md={2} lg={4} className="g-0 libraries-container">
|
||||
{data &&
|
||||
data.sort((a,b) => a.Name-b.Name).map((item) => (
|
||||
data.filter((library) => library.archived ===false || library.archived===showArchived) .sort((a,b) => a.Name-b.Name).map((item) => (
|
||||
<ErrorBoundary key={item.Id} >
|
||||
<LibraryCard data={item} metadata={metadata.find(data => data.Id === item.Id)} base_url={config.hostUrl}/>
|
||||
</ErrorBoundary>
|
||||
|
||||
Reference in New Issue
Block a user