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:
Thegan Govender
2023-11-20 10:59:12 +02:00
parent a74ce2b09e
commit 324f173e3e
7 changed files with 195 additions and 100 deletions

View File

@@ -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);
}

View File

@@ -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);
}
};

View 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);
}
};

View File

@@ -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 = {

View File

@@ -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)
{

View File

@@ -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});
});

View File

@@ -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>