Skip to content

Commit

Permalink
Merge pull request #574 from TomoyukiAota/feature/handle-heif-thumbna…
Browse files Browse the repository at this point in the history
…il-generation-failure

Display the list of files whose thumbnails cannot be generated.
  • Loading branch information
mergify[bot] authored Dec 11, 2024
2 parents 99e1a5d + 2e2f6ad commit 0634f39
Show file tree
Hide file tree
Showing 22 changed files with 464 additions and 89 deletions.
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"leaflet-plugins": "3.4.0",
"mixpanel-browser": "2.45.0",
"ng-mocks": "14.13.1",
"ng-table-virtual-scroll": "1.6.1",
"ngx-echarts": "15.0.3",
"npm-check": "6.0.1",
"npm-run-all": "4.1.5",
Expand Down
23 changes: 15 additions & 8 deletions src-main/thumbnail-generation/thumbnail-generation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as fs from 'fs';
import * as fsExtra from 'fs-extra';
import * as os from 'os';
import * as pathModule from 'path';
import * as physicalCpuCount from 'physical-cpu-count';
import * as workerpool from 'workerpool';
import * as _ from 'lodash';
import { stringArrayToLogText } from '../../src-shared/log/multiline-log-text';
import { removeInvalidThumbnailCache } from '../../src-shared/thumbnail/cache/remove-invalid-thumbnail-cache';
import { createFileForLastModified, getThumbnailFilePath } from '../../src-shared/thumbnail/cache/thumbnail-cache-util';
import { createThumbnailGenerationLogFile, getThumbnailFilePath } from '../../src-shared/thumbnail/cache/thumbnail-cache-util';
import { thumbnailGenerationLogger as logger } from '../../src-shared/thumbnail/generation/thumbnail-generation-logger';
import { ThumbnailFileGenerationArg } from './generate-thumbnail-file-arg-and-result';
import { ThumbnailFileGenerationArg, ThumbnailFileGenerationResult } from './generate-thumbnail-file-arg-and-result';

export function handleThumbnailGenerationIpcRequest(allHeifFilePaths: string[], heifFilePathsToGenerateThumbnail: string[]): void {
if (!allHeifFilePaths || !heifFilePathsToGenerateThumbnail) {
Expand Down Expand Up @@ -36,7 +36,7 @@ class FileForWorker {
function checkFileForWorkerExists(): void {
const filePath = FileForWorker.absoluteFilePath;
logger.info(`The expected file path for worker used during thumbnail generation is "${filePath}"`);
const isFileFound = fs.existsSync(filePath);
const isFileFound = fsExtra.existsSync(filePath);

if (isFileFound) {
logger.info(`The file for worker used during thumbnail generation is found.`);
Expand Down Expand Up @@ -114,10 +114,17 @@ async function runWorkerForThumbnailGeneration(argArray: ThumbnailFileGeneration
return pool
.proxy()
.then(worker => worker.generateThumbnailFile(arg))
.then(result => {
logger.info('Observed completion of worker for thumbnail generation. ' +
`From "${arg.srcFilePath}", a thumbnail file "${arg.outputFilePath}" should have been generated.`);
createFileForLastModified(arg.srcFilePath, arg.outputFileDir, logger);
.then(async result => {
const status = (result as unknown as ThumbnailFileGenerationResult)?.status; // Cast is necessary to avoid the TypeScript compilation error TS2352.
logger.info(`Observed completion of worker for thumbnail generation. Status: ${status}`);
logger.info(`Finished attempting to create a thumbnail file "${arg.outputFilePath}" from "${arg.srcFilePath}".`);
const isThumbnailFileCreated = await fsExtra.pathExists(arg.outputFilePath);
logger.info(`Does the thumbnail file exist? -> ${isThumbnailFileCreated}`);
if(!isThumbnailFileCreated) {
logger.warn(`Failed to create a thumbnail file since it does not exist after an attempt to create it.` +
` Attempted to create a thumbnail file "${arg.outputFilePath}" from "${arg.srcFilePath}".`);
}
await createThumbnailGenerationLogFile(arg.srcFilePath, arg.outputFileDir, isThumbnailFileCreated, logger);
});
});

Expand Down
51 changes: 36 additions & 15 deletions src-shared/thumbnail/cache/thumbnail-cache-util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as fs from 'fs';
import * as fsExtra from 'fs-extra';
import * as os from 'os';
import * as pathModule from 'path';
import { PrependedLogger } from "../../log/create-prepended-logger";
import { PrependedLogger } from '../../log/create-prepended-logger';
import { Logger } from '../../log/logger';

export const plmThumbnailCacheDir = pathModule.join(os.homedir(), '.PlmCache');
Expand Down Expand Up @@ -81,55 +81,76 @@ export function getOriginalFilePath(thumbnailFilePath: string): string {
}

const lastModifiedKey = 'LastModified';
const isThumbnailFileCreatedKey = 'IsThumbnailFileCreated';

export async function createFileForLastModified(srcFilePath: string, thumbnailFileDir: string, logger: PrependedLogger) {
export async function createThumbnailGenerationLogFile(srcFilePath: string, thumbnailFileDir: string, isThumbnailFileCreated: boolean, logger: PrependedLogger) {
const srcFileName = pathModule.basename(srcFilePath);
const lastModified = fs.statSync(srcFilePath).mtime.toISOString();
const lastModified = fsExtra.statSync(srcFilePath).mtime.toISOString();
const fileContentObj = {};
fileContentObj[lastModifiedKey] = lastModified;
fileContentObj[isThumbnailFileCreatedKey] = isThumbnailFileCreated;
const fileContentStr = JSON.stringify(fileContentObj, null, 2);
const logFilePath = pathModule.join(thumbnailFileDir, `${srcFileName}.log.json`);

try {
await fs.promises.writeFile(logFilePath, fileContentStr);
await fsExtra.ensureFile(logFilePath);
await fsExtra.promises.writeFile(logFilePath, fileContentStr);
} catch (error) {
logger.error(`Failed to write file for last modified "${lastModified}" for "${srcFileName}" in "${logFilePath}". error: ${error}`, error);
logger.error(`Failed to write the file in "${logFilePath}" which is the thumbnail generation result for "${srcFilePath}". error: ${error}`, error);
return;
}

logger.info(`Wrote a file for last modified "${lastModified}" for "${srcFileName}" in ${logFilePath}`);
logger.info(`Wrote the file in "${logFilePath}" which is the thumbnail generation result for "${srcFilePath}".`);
}

export function isAttemptToGenerateThumbnailFinished(srcFilePath: string): boolean {
const lastModifiedMatch = lastModifiedMatchBetweenSrcFileAndThumbnailGenerationLogFile(srcFilePath);
return lastModifiedMatch;
}

export function isThumbnailCacheAvailable(srcFilePath: string): boolean {
if (!srcFilePath)
return false;

const srcFileName = pathModule.basename(srcFilePath);

const { thumbnailFilePath } = getThumbnailFilePath(srcFilePath);
const thumbnailFileExists = fs.existsSync(thumbnailFilePath);
const thumbnailFileExists = fsExtra.existsSync(thumbnailFilePath);
if (!thumbnailFileExists)
return false;

const lastModifiedMatch = lastModifiedMatchBetweenSrcFileAndThumbnailGenerationLogFile(srcFilePath);
return lastModifiedMatch;
}

export function lastModifiedMatchBetweenSrcFileAndThumbnailGenerationLogFile(srcFilePath: string): boolean {
if (!srcFilePath)
return false;

const logFilePath = getThumbnailLogFilePath(srcFilePath);
const logFileExists = fs.existsSync(logFilePath);
const logFileExists = fsExtra.existsSync(logFilePath);
if (!logFileExists)
return false;

let fileContentStr;
try {
fileContentStr = fs.readFileSync(logFilePath, 'utf8');
fileContentStr = fsExtra.readFileSync(logFilePath, 'utf8');
} catch (error) {
Logger.warn(`Failed to read the log file "${logFilePath}" which is the thumbnail generation result for "${srcFilePath}". error: ${error}`, error);
return false;
}

let fileContentObj;
try {
fileContentObj = JSON.parse(fileContentStr);
} catch (error) {
Logger.error(`Failed to read log file for ${srcFileName}. Log file location is "${logFilePath}". error: ${error}`, error);
Logger.warn(`Failed to parse the content of the log file "${logFilePath}" as JSON format. The log file is for thumbnail generation result of "${srcFilePath}". error: ${error}`, error);
return false;
}

const fileContentObj = JSON.parse(fileContentStr);
const lastModifiedFromLogFile = fileContentObj[lastModifiedKey];
if (!lastModifiedFromLogFile)
return false;

const lastModifiedFromSrcFile = fs.statSync(srcFilePath).mtime.toISOString();
const lastModifiedFromSrcFile = fsExtra.statSync(srcFilePath).mtime.toISOString();
const lastModifiedMatch = lastModifiedFromLogFile === lastModifiedFromSrcFile;
return lastModifiedMatch;
}
12 changes: 12 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'reflect-metadata';

import { ScrollingModule } from '@angular/cdk/scrolling';
import { HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
Expand All @@ -13,13 +14,16 @@ import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatSelectModule } from '@angular/material/select';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTreeModule } from '@angular/material/tree';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { NgxEchartsModule } from 'ngx-echarts';
import { TableVirtualScrollModule } from 'ng-table-virtual-scroll';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
Expand All @@ -41,6 +45,8 @@ import { AboutBoxComponent } from './about-box/about-box.component';
import { WelcomeDialogComponent } from './welcome-dialog/welcome-dialog.component';
import { SettingsDialogComponent } from './settings-dialog/settings-dialog.component';
import { ThumbnailGenerationStatusBarComponent } from './thumbnail-generation/status-bar/component/thumbnail-generation-status-bar.component';
import { ThumbnailGenerationErrorDialogComponent } from './thumbnail-generation/error-dialog/thumbnail-generation-error-dialog.component';
import { ThumbnailGenerationErrorTableComponent } from './thumbnail-generation/error-dialog/table/thumbnail-generation-error-table.component';
import { AppearanceSettingsComponent } from './settings-dialog/appearance-settings/appearance-settings.component';
import { DateTimeSettingsComponent } from './settings-dialog/date-time-settings/date-time-settings.component';
import { OsSettingsComponent } from './settings-dialog/os-settings/os-settings.component';
Expand Down Expand Up @@ -74,6 +80,8 @@ export function HttpLoaderFactory(http: HttpClient) {
DateTimeTakenChartConfigComponent,
SelectPhotosWithinZoomComponent,
ThumbnailGenerationStatusBarComponent,
ThumbnailGenerationErrorDialogComponent,
ThumbnailGenerationErrorTableComponent,
AppearanceSettingsComponent,
DateTimeSettingsComponent,
OsSettingsComponent,
Expand Down Expand Up @@ -103,10 +111,14 @@ export function HttpLoaderFactory(http: HttpClient) {
MatMenuModule,
MatProgressBarModule,
MatSelectModule,
MatSortModule,
MatTableModule,
MatTreeModule,
NgxEchartsModule.forRoot({
echarts: () => import('echarts'),
}),
ScrollingModule,
TableVirtualScrollModule,
],
providers: [
provideHttpClient(withInterceptorsFromDi())
Expand Down
4 changes: 2 additions & 2 deletions src/app/photo-info-viewer/thumbnail-element.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Analytics } from '../../../src-shared/analytics/analytics';
import { FilenameExtension } from '../../../src-shared/filename-extension/filename-extension';
import { isFilePathTooLongOnWindows, maxFilePathLengthOnWindows } from '../../../src-shared/max-file-path-length-on-windows/max-file-path-length-on-windows';
import { getThumbnailFilePath, isThumbnailCacheAvailable } from '../../../src-shared/thumbnail/cache/thumbnail-cache-util';
import { getThumbnailFilePath, isAttemptToGenerateThumbnailFinished } from '../../../src-shared/thumbnail/cache/thumbnail-cache-util';
import { IconDataUrl } from '../../assets/icon-data-url';
import { Photo } from '../shared/model/photo.model';
import { PhotoViewerLauncher } from '../photo-viewer-launcher/photo-viewer-launcher';
Expand Down Expand Up @@ -58,7 +58,7 @@ export class ThumbnailElement {
thumbnailElement.classList.add(cssClassAppliedToGeneratingThumbnailImage);

const intervalId = setInterval(() => {
if (isThumbnailCacheAvailable(photo.path)) {
if (isAttemptToGenerateThumbnailFinished(photo.path)) {
thumbnailElement.classList.remove(cssClassAppliedToGeneratingThumbnailImage);

// Apply and remove the CSS class in order for thumbnail to fade in just once after its generation is done.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</div>
<div class="location-table">
<div class="location-label">Cache location:</div>
<div class="location-value anchor-tag-style" (click)="openThumbnailCacheLocation()">{{ thumbnailCacheLocation }}</div>
<div class="location-value anchor-tag-like-text" (click)="openThumbnailCacheLocation()">{{ thumbnailCacheLocation }}</div>
</div>
<div class="delete-button-description">This application will restart after deleting the thumbnail cache.</div>
<button mat-flat-button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
@import "/src/styles/anchor-tag-like-text";
@import "../settings-styles";

.setting-title {
Expand Down Expand Up @@ -33,15 +34,8 @@
.location-value { grid-area: location-value; }
}

// anchor-tag-style is to give styles like <a> tag to the specified text without using <a> tag.
.anchor-tag-style {
color: -webkit-link;
cursor: pointer;
text-decoration: underline;

&:active{
color: -webkit-activelink;
}
.anchor-tag-like-text {
@include anchor-tag-like-text;
}

.delete-button-description {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<mat-form-field>
<input matInput (keyup)="applyFilter($any($event.target).value)" placeholder="Filter">
</mat-form-field>

<cdk-virtual-scroll-viewport tvsItemSize="48"
class="mat-elevation-z2">

<table mat-table [dataSource]="dataSource" matSort class="mat-elevation-z8">

<!-- Position Column -->
<ng-container matColumnDef="position">
<th mat-header-cell
*matHeaderCellDef
mat-sort-header
class="position-column-header-cell">
No.
</th>
<td mat-cell
*matCellDef="let element"
class="position-column-cell">
{{ element.position }}
</td>
</ng-container>

<!-- File Column -->
<ng-container matColumnDef="filePath">
<th mat-header-cell *matHeaderCellDef mat-sort-header>File</th>
<td mat-cell *matCellDef="let element">
<div class="file-path-and-buttons"
(mouseenter)="buttons.style.opacity = '1'"
(mouseleave)="buttons.style.opacity = '0'">
<div class="file-path">{{ element.filePath }}</div>
<div class="buttons" #buttons>
<button (click)="handleOpenFileButtonClicked(element.filePath)">
<img [src]="launchExternalAppIconDataUrl"
alt="Open File"
title="Open File">
</button>
<button (click)="handleOpenFolderButtonClicked(element.filePath)">
<img [src]="folderIconDataUrl"
alt="Open Folder"
title="Open Folder">
</button>
</div>
</div>
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

</table>

</cdk-virtual-scroll-viewport>
Loading

0 comments on commit 0634f39

Please sign in to comment.