Skip to content

Commit

Permalink
feat: allow for MP4 conversion (#3756)
Browse files Browse the repository at this point in the history
* feat: allow for MP4 conversion

* fix: dont reconvert if converted file already exists

* refactor: optimize imports

* chore: remove console log

* chore: add packages to electron deps

* chore: improve migrations

* fix: electronApi issues

* fix: improvements to font url code

---------

Co-authored-by: Manoah Tervoort <[email protected]>
Co-authored-by: Manoah Tervoort <[email protected]>
  • Loading branch information
3 people authored Dec 19, 2024
1 parent 2e3f093 commit 0b39f08
Show file tree
Hide file tree
Showing 23 changed files with 497 additions and 111 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"Kiswahili",
"Kreyòl",
"langwritten",
"libx",
"linux",
"lmdv",
"lorenpajarits",
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ In addition, M³ itself has been translated into several languages by many volun
<!-- prettier-ignore-start -->
<!-- CROWDIN-TRANSLATIONS-PROGRESS-ACTION-START -->


#### Available

<table><tr><td align="center" valign="top"><img width="30px" height="30px" title="Chinese Simplified" alt="Chinese Simplified" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/zh-CN.png"></div><div align="center" valign="top">100%</td><td align="center" valign="top"><img width="30px" height="30px" title="Dutch" alt="Dutch" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/nl.png"></div><div align="center" valign="top">100%</td><td align="center" valign="top"><img width="30px" height="30px" title="Italian" alt="Italian" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/it.png"></div><div align="center" valign="top">100%</td><td align="center" valign="top"><img width="30px" height="30px" title="Portuguese" alt="Portuguese" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/pt-PT.png"></div><div align="center" valign="top">100%</td><td align="center" valign="top"><img width="30px" height="30px" title="Portuguese, Brazilian" alt="Portuguese, Brazilian" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/pt-BR.png"></div><div align="center" valign="top">100%</td></tr><tr><td align="center" valign="top"><img width="30px" height="30px" title="Spanish" alt="Spanish" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/es-ES.png"></div><div align="center" valign="top">100%</td><td align="center" valign="top"><img width="30px" height="30px" title="Swedish" alt="Swedish" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/sv-SE.png"></div><div align="center" valign="top">100%</td><td align="center" valign="top"><img width="30px" height="30px" title="Ukrainian" alt="Ukrainian" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/uk.png"></div><div align="center" valign="top">100%</td><td align="center" valign="top"><img width="30px" height="30px" title="Estonian" alt="Estonian" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/et.png"></div><div align="center" valign="top">99%</td><td align="center" valign="top"><img width="30px" height="30px" title="French" alt="French" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/fr.png"></div><div align="center" valign="top">99%</td></tr><tr><td align="center" valign="top"><img width="30px" height="30px" title="Slovenian" alt="Slovenian" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/sl.png"></div><div align="center" valign="top">99%</td><td align="center" valign="top"><img width="30px" height="30px" title="German" alt="German" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/de.png"></div><div align="center" valign="top">98%</td><td align="center" valign="top"><img width="30px" height="30px" title="Hungarian" alt="Hungarian" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/hu.png"></div><div align="center" valign="top">98%</td><td align="center" valign="top"><img width="30px" height="30px" title="Swahili" alt="Swahili" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/sw.png"></div><div align="center" valign="top">98%</td><td align="center" valign="top"><img width="30px" height="30px" title="Russian" alt="Russian" src="https://raw.githubusercontent.com/benjaminjonard/crowdin-translations-progress-action/1.0/flags/ru.png"></div><div align="center" valign="top">94%</td></tr></table>
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@
"dompurify": "^3.2.3",
"electron-dl-manager": "^3.2.1",
"electron-updater": "^6.3.9",
"fluent-ffmpeg": "^2.1.3",
"fs-extra": "^11.2.0",
"heic-convert": "^2.1.0",
"image-size": "^1.1.1",
"is-online": "^11.0.0",
"music-metadata": "^10.6.4",
"obs-websocket-js": "^5.0.6",
Expand Down Expand Up @@ -79,6 +81,7 @@
"@types/better-sqlite3": "^7.6.12",
"@types/decompress": "^4.2.7",
"@types/dompurify": "^3.2.0",
"@types/fluent-ffmpeg": "^2",
"@types/fs-extra": "^11.0.4",
"@types/heic-convert": "^2.1.0",
"@types/node": "^20.17.10",
Expand Down
10 changes: 6 additions & 4 deletions quasar.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,26 +162,28 @@ export default defineConfig((ctx) => {
},
extendPackageJson(pkg) {
// All dependencies required by the main and preload scripts need to be listed here
const electronDeps = [
const electronDeps = new Set([
'@numairawan/video-duration',
'@sentry/electron',
'better-sqlite3',
'chokidar',
'decompress',
'countries-and-timezones',
'decompress',
'electron-dl-manager',
'electron-updater',
'fluent-ffmpeg',
'fs-extra',
'heic-convert',
'image-size',
'is-online',
'music-metadata',
'pdfjs-dist',
'upath',
];
]);

// Remove unneeded dependencies from production build
Object.keys(pkg.dependencies).forEach((dep) => {
if (!electronDeps.includes(dep)) {
if (!electronDeps.has(dep)) {
console.log(`Removing dependency: ${dep}`);
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete pkg.dependencies[dep];
Expand Down
1 change: 1 addition & 0 deletions src-electron/electron-preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const electronApi: ElectronApi = {
closeWebsiteWindow,
convertHeic,
convertPdfToImages,
createVideoFromNonVideo: (f, fP) => invoke('createVideoFromNonVideo', f, fP),
decompress,
downloadFile: (u, sD, dF, lP) => invoke('downloadFile', u, sD, dF, lP),
executeQuery,
Expand Down
111 changes: 111 additions & 0 deletions src-electron/main/ffmpeg.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { exists } from 'fs-extra';
import { FULL_HD } from 'src/constants/media';
import path from 'upath';

const { changeExt } = path;

export const createVideoFromNonVideo = async (
originalFile: string,
ffmpegPath: string,
) => {
const convertedFilePath = changeExt(originalFile, '.mp4');

if (await exists(convertedFilePath)) {
return convertedFilePath;
}

const { default: ffmpeg } = await import('fluent-ffmpeg');

ffmpeg.setFfmpegPath(ffmpegPath);

if (originalFile.toLowerCase().endsWith('.mp3')) {
await new Promise<void>((resolve, reject) => {
ffmpeg(originalFile)
.noVideo()
.save(convertedFilePath)
.on('end', () => {
resolve();
})
.on('error', (err) => {
reject(err);
});
});
} else {
const { default: sizeOf } = await import('image-size');
const { height, orientation, width } = sizeOf(originalFile);

let adjustedWidth = width;
let adjustedHeight = height;

if (orientation && orientation >= 5) {
[adjustedWidth, adjustedHeight] = [height, width];
}

if (adjustedWidth && adjustedHeight) {
let max = [undefined, Math.min(FULL_HD.height, adjustedHeight)];
if (FULL_HD.height / FULL_HD.width > adjustedHeight / adjustedWidth) {
max = [Math.min(FULL_HD.width, adjustedWidth), undefined];
}
const convertedDimensions = resize(
adjustedWidth,
adjustedHeight,
max[0],
max[1],
);

if (!convertedDimensions) {
throw new Error('Could not determine dimensions of image.');
}

await new Promise<void>((resolve, reject) => {
ffmpeg(originalFile)
.inputOptions('-loop 1')
.inputFormat('image2')
.videoCodec('libx264')
.size(`${convertedDimensions.width}x${convertedDimensions.height}`)
// .outputOptions('format=yuv420p', '-r 30')
.outputOptions('-t 5')
.save(convertedFilePath)
.on('end', () => {
resolve();
})
.on('error', (err) => {
reject(err);
});
});
} else {
throw new Error('Could not determine dimensions of image.');
}
}

return convertedFilePath;
};

const resize = (
x: number,
y: number,
xMax?: number,
yMax?: number,
): { height: number; width: number } => {
// Set default values for xMax and yMax if they are undefined
const maxX = xMax ?? Infinity; // Use Infinity if no max value is provided
const maxY = yMax ?? Infinity;

if (maxX === Infinity && maxY === Infinity) {
throw new Error('No maximum values given.');
}

if (maxX !== Infinity && (maxY === Infinity || x / y > maxX / maxY)) {
// Width constrained or aspect ratio favors width.
return {
height: Math.round((maxX * y) / x),
width: maxX,
};
}

// Height constrained or aspect ratio favors height.
return {
height: maxY,
width: Math.round((maxY * x) / y),
};
};
7 changes: 7 additions & 0 deletions src-electron/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {

import { PLATFORM } from '../constants';
import { downloadFile } from './downloads';
import { createVideoFromNonVideo } from './ffmpeg';
import {
openFileDialog,
openFolderDialog,
Expand Down Expand Up @@ -220,6 +221,12 @@ handleIpcInvoke(
readDirectory(dir, withSizes, recursive),
);

handleIpcInvoke(
'createVideoFromNonVideo',
async (_e, path: string, ffmpegPath: string) =>
createVideoFromNonVideo(path, ffmpegPath),
);

handleIpcInvoke(
'downloadFile',
async (
Expand Down
3 changes: 2 additions & 1 deletion src-electron/preload/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import type { IOptions } from 'music-metadata';
import type { VideoDuration } from 'src/types';

import path from 'upath';
import url from 'url';

import { capturePreloadError } from './log';
Expand Down Expand Up @@ -41,5 +42,5 @@ export const pathToFileURL = (path: string) => {
export const fileUrlToPath = (fileurl: string) => {
if (!fileurl) return '';
if (!isFileUrl(fileurl)) return fileurl;
return url.fileURLToPath(fileurl);
return path.normalize(url.fileURLToPath(fileurl));
};
2 changes: 1 addition & 1 deletion src/components/form-inputs/ToggleInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const customDisabled = computed(() => {
);
});
const model = defineModel<boolean>({ required: true });
const model = defineModel<boolean | undefined>({ required: true });
// Setup component
watch(model, () => {
Expand Down
8 changes: 8 additions & 0 deletions src/constants/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ export const settingsDefinitions: SettingsItems = {
type: 'path',
unless: 'disableMediaFetching',
},
convertFilesToMp4: {
depends: ['enableMediaDisplayButton', 'enableMediaAutoExport'],
group: 'advanced',
subgroup: 'mediaExport',
type: 'toggle',
unless: 'disableMediaFetching',
},
musicVolume: {
actions: ['setBackgroundMusicVolume'],
depends: 'enableMusicButton',
Expand Down Expand Up @@ -341,6 +348,7 @@ export const defaultSettings: SettingsValues = {
baseUrl: 'jw.org',
cacheFolder: null,
congregationName: null,
convertFilesToMp4: false,
coWeek: null,
darkMode: 'auto',
disableMediaFetching: false,
Expand Down
2 changes: 0 additions & 2 deletions src/helpers/cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { errorCatcher } from 'src/helpers/error-catcher';
// import { getAdditionalMediaPath, removeEmptyDirs } from 'src/helpers/fs';
// import { useJwStore } from 'src/stores/jw';

// const { fileUrlToPath, fs, readDirectory, path } = window.electronApi;

export const cleanLocalStorage = () => {
// try {
// ['additionalMediaMaps', 'mediaSort'].forEach((key) => {
Expand Down
30 changes: 24 additions & 6 deletions src/helpers/export-media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type PQueue from 'p-queue';
import type { MediaSection } from 'src/types';

import { errorCatcher } from 'src/helpers/error-catcher';
import { setupFFmpeg } from 'src/helpers/fs';
import { mapOrder } from 'src/helpers/jw-media';
import { useCurrentStateStore } from 'src/stores/current-state';
import { useJwStore } from 'src/stores/jw';
import { datesAreSame, formatDate } from 'src/utils/date';
import { trimFilepathAsNeeded } from 'src/utils/fs';
import { pad } from 'src/utils/general';

import { mapOrder } from './jw-media';
import { isVideo } from 'src/utils/media';

export const addDayToExportQueue = async (targetDate?: Date) => {
if (!folderExportQueue) {
Expand Down Expand Up @@ -87,13 +88,30 @@ const exportDayToFolder = async (targetDate?: Date) => {
try {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const m = dynamicMediaFiltered[i]!;
const sourceFilePath = window.electronApi.fileUrlToPath(m.fileUrl);
let sourceFilePath = window.electronApi.fileUrlToPath(m.fileUrl);
if (
!sourceFilePath ||
!(await window.electronApi.fs.exists(sourceFilePath))
)
continue;

if (
!isVideo(sourceFilePath) &&
currentStateStore.currentSettings?.convertFilesToMp4
) {
try {
const ffmpegPath = await setupFFmpeg();
const convertedFilePath =
await window.electronApi.createVideoFromNonVideo(
sourceFilePath,
ffmpegPath,
);
sourceFilePath = convertedFilePath;
} catch (error) {
errorCatcher(error);
}
}

if (!sections[m.section]) {
sections[m.section] = Object.keys(sections).length + 1;
}
Expand All @@ -109,11 +127,11 @@ const exportDayToFolder = async (targetDate?: Date) => {
(m.title
? sanitize(
m.title.replace(
window.electronApi.path.extname(m.fileUrl),
window.electronApi.path.extname(sourceFilePath),
'',
),
) + window.electronApi.path.extname(m.fileUrl)
: window.electronApi.path.basename(m.fileUrl)),
) + window.electronApi.path.extname(sourceFilePath)
: window.electronApi.path.basename(sourceFilePath)),
),
);
const fileBaseName = window.electronApi.path.basename(destFilePath);
Expand Down
Loading

0 comments on commit 0b39f08

Please sign in to comment.