Reworking Cards in the App so that they use the new FileGroup.files and .data.

This commit is contained in:
Roland 2025-12-24 12:30:04 -07:00
parent b6e82cde26
commit 5d50562768
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
5 changed files with 62 additions and 26 deletions

View file

@ -34,6 +34,7 @@ import {
mimetypeColor,
PageContainer,
PreviewPath,
resolveDataPath,
SearchInput,
SortButton,
TabLinks,
@ -104,11 +105,16 @@ function ArchivePage() {
</>
}
const {data, size} = archiveFile;
const {data, size, directory} = archiveFile;
const singlefileUrl = data.singlefile_path ? `/media/${encodeMediaPath(data.singlefile_path)}` : null;
const readabilityUrl = data.readability_path ? `/media/${encodeMediaPath(data.readability_path)}` : null;
const screenshotUrl = data.screenshot_path ? `/media/${encodeMediaPath(data.screenshot_path)}` : null;
// Resolve data paths (filename-only) to full relative paths using directory
const singlefilePath = resolveDataPath(data.singlefile_path, directory);
const readabilityPath = resolveDataPath(data.readability_path, directory);
const screenshotPath = resolveDataPath(data.screenshot_path, directory);
const singlefileUrl = singlefilePath ? `/media/${encodeMediaPath(singlefilePath)}` : null;
const readabilityUrl = readabilityPath ? `/media/${encodeMediaPath(readabilityPath)}` : null;
const screenshotUrl = screenshotPath ? `/media/${encodeMediaPath(screenshotPath)}` : null;
const singlefileButton = <ExternalCardLink to={singlefileUrl}>
<Button content='View' color='violet'/>
@ -235,9 +241,11 @@ function ArchivePage() {
</Tab.Pane>
};
const localPreviewPath = (path, mimetype) => {
if (path) {
return <PreviewPath path={path} mimetype={mimetype} taggable={false}>{path}</PreviewPath>
// Helper to resolve and preview a data path
const localPreviewPath = (dataPath, mimetype) => {
const resolvedPath = resolveDataPath(dataPath, directory);
if (resolvedPath) {
return <PreviewPath path={resolvedPath} mimetype={mimetype} taggable={false}>{resolvedPath}</PreviewPath>
} else {
return 'Unknown'
}
@ -258,8 +266,8 @@ function ArchivePage() {
{localPreviewPath(data.readability_json_path, 'application/json')}
<Header as={'h3'}>Screenshot File</Header>
{data.screenshot_path ?
archiveFileLink(data.screenshot_path)
{screenshotPath ?
archiveFileLink(screenshotPath)
: 'Unknown'}
<Header as={'h3'}>Directory</Header>
@ -321,10 +329,14 @@ function ArchivePage() {
export function ArchiveCard({file}) {
const {s} = useContext(ThemeContext);
const {data} = file;
const {data, directory} = file;
const imageSrc = data.screenshot_path ? `/media/${encodeMediaPath(data.screenshot_path)}` : null;
const singlefileUrl = data.singlefile_path ? `/media/${encodeMediaPath(data.singlefile_path)}` : null;
// Resolve data paths (filename-only) to full relative paths using directory
const screenshotPath = resolveDataPath(data.screenshot_path, directory);
const singlefilePath = resolveDataPath(data.singlefile_path, directory);
const imageSrc = screenshotPath ? `/media/${encodeMediaPath(screenshotPath)}` : null;
const singlefileUrl = singlefilePath ? `/media/${encodeMediaPath(singlefilePath)}` : null;
let screenshot = <Card.Icon><FileIcon file={file}/></Card.Icon>;
const imageLabel = file.tags && file.tags.length ? taggedImageLabel : null;

View file

@ -656,11 +656,27 @@ export function CardGroupCentered(props) {
</div>
}
/**
* Resolve a filename-only path to a full relative path using the file's directory.
* Paths in FileGroup.data are stored as filenames only for performance (fast moves/renames).
* This function resolves them to full relative paths suitable for /media/ URLs.
*
* @param {string} path - The path to resolve (may be filename-only or already a full path)
* @param {string} directory - The directory to prepend if path is filename-only
* @returns {string|null} - The resolved path, or null if path is falsy
*/
export function resolveDataPath(path, directory) {
if (!path) return null;
if (path.includes('/')) return path; // Already a full relative path
return directory ? `${directory}/${path}` : path;
}
export function findPosterPath(file) {
if (!file) {
return;
}
const {files, poster_path, cover_path, screenshot_path, video} = file;
const {files, poster_path, cover_path, screenshot_path, video, directory} = file;
if (poster_path) {
return poster_path;
}
@ -670,18 +686,24 @@ export function findPosterPath(file) {
if (screenshot_path) {
return screenshot_path;
}
if (file['data'] && file['data']['cover_path']) {
// Ebook.
return file['data']['cover_path'];
}
if (file['data'] && file['data']['poster_path']) {
// PDF.
return file['data']['poster_path'];
}
// video.poster_path is already a full relative path (from Video.__json__)
if (video && video['poster_path']) {
// file is a video model.
return video['poster_path'];
}
// data paths are filename-only, need to resolve with directory
if (file['data'] && file['data']['cover_path']) {
// Ebook.
return resolveDataPath(file['data']['cover_path'], directory);
}
if (file['data'] && file['data']['poster_path']) {
// PDF or Video (cached).
return resolveDataPath(file['data']['poster_path'], directory);
}
if (file['data'] && file['data']['screenshot_path']) {
// Archive.
return resolveDataPath(file['data']['screenshot_path'], directory);
}
// files[].path is already resolved by my_files() on the backend
if (!_.isEmpty(files)) {
for (let i = 0; i < files.length; i++) {
const file = files[i];

View file

@ -8,7 +8,8 @@ from sqlalchemy.orm.collections import InstrumentedList
from modules.videos.errors import UnknownVideo, UnknownChannel
from wrolpi.captions import read_captions
from wrolpi.common import Base, ModelHelper, logger, get_media_directory, background_task, replace_file
from wrolpi.common import Base, ModelHelper, logger, get_media_directory, get_relative_to_media_directory, \
background_task, replace_file
from wrolpi.db import get_db_curs, get_db_session
from wrolpi.downloader import Download
from wrolpi.files.lib import refresh_files, split_path_stem_and_suffix

View file

@ -152,10 +152,11 @@ async def pdf_modeler():
file_group.model = 'pdf'
# Find an image with the same stem to use as the poster.
# Store as filename only (consistent with other models, enables fast moves/renames).
poster_files = file_group.my_poster_files()
if poster_files:
file_group.data = file_group.data or {}
file_group.data['poster_path'] = str(poster_files[0]['path'])
file_group.data['poster_path'] = poster_files[0]['path'].name
except Exception as e:
logger.error(f'Failed to index PDF {pdf_file}', exc_info=e)
if PYTEST:

View file

@ -90,5 +90,5 @@ async def test_pdf_poster(test_session, example_pdf):
file_group = test_session.query(FileGroup).one()
assert file_group.indexed is True
assert file_group.data is not None
# FancyJSON converts paths back to pathlib.Path when reading from DB.
assert file_group.data.get('poster_path') == poster_path
# data['poster_path'] stores filename only (for fast moves/renames).
assert file_group.data.get('poster_path') == poster_path.name