diff --git a/copy.js b/copy.js new file mode 100644 index 0000000..f26017a --- /dev/null +++ b/copy.js @@ -0,0 +1,9 @@ +const fs = require('fs-extra'); +const path = require('path'); + +const sourceDir = path.join(__dirname, 'renderer', 'public'); +const destDir = path.join(__dirname, 'app'); + +fs.copy(sourceDir, destDir) + .then(() => console.log('Files copied successfully!')) + .catch(err => console.error(err)); diff --git a/main/background.ts b/main/background.ts index 4b3799b..064523a 100644 --- a/main/background.ts +++ b/main/background.ts @@ -13,6 +13,7 @@ import { import fs from "fs"; import path from "path"; import {getTokenizer} from "kuromojin"; +import {getSubtitles} from "./helpers/getSubtitles"; const isProd: boolean = process.env.NODE_ENV === 'production'; @@ -118,6 +119,16 @@ if (isProd) { ipcMain.handle('tags', (event) => { return JMDict.tags; }) + ipcMain.handle('getYoutubeSubtitle', async (event, videoID, lang) => { + // Fetching Subtitles + try { + return await getSubtitles({videoID, lang}) + } catch (error) { + console.error('Error fetching subtitles:', error); + return [] + } + + }) ipcMain.handle('pickDirectory', async (event) => { return await dialog.showOpenDialog({ properties: @@ -160,7 +171,7 @@ if (isProd) { if (filePath && !canceled) { fs.writeFile(filePath, saveData, (err) => { if (err) throw err; - console.log('The file has been saved!'); + console.info('The file has been saved!'); }); } @@ -271,7 +282,7 @@ if (isProd) { getTokenizer({dicPath: path.join(__dirname, 'dict/')}).then(loadedTokenizer => { tokenizer = loadedTokenizer; }).catch(e => { - console.log(e) + console.error(e) }) ipcMain.handle('tokenizeUsingKuromoji', async (event, sentence) => { return tokenizer.tokenizeForSentence(sentence); diff --git a/main/helpers/getSubtitles.ts b/main/helpers/getSubtitles.ts new file mode 100644 index 0000000..d127a37 --- /dev/null +++ b/main/helpers/getSubtitles.ts @@ -0,0 +1,70 @@ +/* @flow */ + +import axios from 'axios'; +import {find} from 'lodash'; +import {decode} from 'html-entities'; + +function stripTags(input, allowedTags = [], replacement = '') { + // Create a string of allowed tags, joined by '|' + let tags = allowedTags.join('|'); + + // Create a new RegExp object + let regex = new RegExp(`<(?!\/?(${tags})[^>]*)\/?.*?>`, 'g'); + + // Replace disallowed tags with replacement string + return input.replace(regex, replacement); +} + +export async function getSubtitles({videoID, lang = 'ja'}) { + const {data} = await axios.get( + `https://youtube.com/watch?v=${videoID}` + ); + + // * ensure we have access to captions data + if (!data.includes('captionTracks')) + throw new Error(`Could not find captions for video: ${videoID}`); + + const regex = /({"captionTracks":.*isTranslatable":(true|false)}])/; + const [match] = regex.exec(data); + const {captionTracks} = JSON.parse(`${match}}`); + + const subtitle = + find(captionTracks, { + vssId: `.${lang}`, + }) || + find(captionTracks, { + vssId: `a.${lang}`, + }) || + find(captionTracks, ({vssId}) => vssId && vssId.match(`.${lang}`)); + + // * ensure we have found the correct subtitle lang + if (!subtitle || (subtitle && !subtitle.baseUrl)) + throw new Error(`Could not find ${lang} captions for ${videoID}`); + + const {data: transcript} = await axios.get(subtitle.baseUrl); + return transcript + .replace('', '') + .replace('', '') + .split('') + .filter(line => line && line.trim()) + .map(line => { + const startRegex = /start="([\d.]+)"/; + const durRegex = /dur="([\d.]+)"/; + + const [, start] = startRegex.exec(line); + const [, dur] = durRegex.exec(line); + + const htmlText = line + .replace(//, '') + .replace(/&/gi, '&') + .replace(/<\/?[^>]+(>|$)/g, ''); + const decodedText = decode(htmlText); + const text = stripTags(decodedText); + + return { + start, + dur, + text, + }; + }); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4fbb0d0..a0243d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "miteiru", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "miteiru", - "version": "1.3.0", + "version": "1.4.0", "hasInstallScript": true, "dependencies": { "@electron/asar": "^3.2.4", @@ -14,9 +14,12 @@ "@plussub/srt-vtt-parser": "^1.1.0", "ass-compiler": "^0.1.8", "async-await-queue": "^2.1.4", + "axios": "^1.4.0", "detect-file-encoding-and-language": "^2.4.0", "electron-serve": "^1.1.0", "electron-store": "^8.1.0", + "fs-extra": "^11.1.1", + "html-entities": "^2.4.0", "html-react-parser": "^3.0.16", "iconv-lite": "^0.6.3", "jmdict-simplified-node": "^1.1.2", @@ -24,12 +27,12 @@ "patch-package": "^7.0.0", "react-awesome-button": "^7.0.5", "react-colorful": "^5.6.1", - "react-dropzone": "^14.2.3", "react-smooth-collapse": "^2.1.2", "react-video-seek-slider": "^6.0.4", "shunou": "^1.0.2", "styled-components": "^6.0.3", - "video.js": "^8.3.0" + "video.js": "^8.3.0", + "videojs-youtube": "^3.0.1" }, "devDependencies": { "@types/node": "^16.18.38", @@ -1895,6 +1898,32 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -2031,6 +2060,19 @@ "node": ">=12.0.0" } }, + "node_modules/@electron/osx-sign/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@electron/rebuild": { "version": "3.2.13", "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-3.2.13.tgz", @@ -2124,6 +2166,20 @@ "node": ">=10" } }, + "node_modules/@electron/rebuild/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@electron/rebuild/node_modules/got": { "version": "11.8.6", "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", @@ -2995,6 +3051,38 @@ "node": ">= 10" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -3546,6 +3634,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/aes-decrypter": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-4.0.1.tgz", @@ -3768,6 +3867,20 @@ "balanced-match": "^1.0.0" } }, + "node_modules/app-builder-lib/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/app-builder-lib/node_modules/isbinaryfile": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.0.tgz", @@ -3991,8 +4104,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -4010,14 +4122,6 @@ "node": ">=10.12.0" } }, - "node_modules/attr-accept": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", - "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", - "engines": { - "node": ">=4" - } - }, "node_modules/autoprefixer": { "version": "10.4.14", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", @@ -4072,6 +4176,16 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz", @@ -4376,6 +4490,20 @@ "node": ">=12.0.0" } }, + "node_modules/builder-util/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -4801,7 +4929,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4965,6 +5092,14 @@ "buffer": "^5.1.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5286,7 +5421,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -5345,6 +5479,17 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-compare": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-3.3.0.tgz", @@ -5390,6 +5535,20 @@ "dmg-license": "^1.0.11" } }, + "node_modules/dmg-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/dmg-license": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/dmg-license/-/dmg-license-1.0.11.tgz", @@ -5590,6 +5749,20 @@ "node": ">=14.0.0" } }, + "node_modules/electron-builder/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/electron-publish": { "version": "24.5.0", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-24.5.0.tgz", @@ -5605,6 +5778,20 @@ "mime": "^2.5.2" } }, + "node_modules/electron-publish/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/electron-serve": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/electron-serve/-/electron-serve-1.1.0.tgz", @@ -6565,17 +6752,6 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/file-selector": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", - "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", - "dependencies": { - "tslib": "^2.4.0" - }, - "engines": { - "node": ">= 12" - } - }, "node_modules/filelist": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", @@ -6692,6 +6868,25 @@ "dev": true, "peer": true }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -6733,7 +6928,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6757,16 +6951,16 @@ } }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-minipass": { @@ -7243,6 +7437,21 @@ "htmlparser2": "8.0.2" } }, + "node_modules/html-entities": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, "node_modules/html-react-parser": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/html-react-parser/-/html-react-parser-3.0.16.tgz", @@ -8395,6 +8604,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/make-fetch-happen": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", @@ -8486,7 +8703,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -8495,7 +8711,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -9831,6 +10046,11 @@ "dev": true, "optional": true }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -9935,22 +10155,6 @@ "react": "^18.2.0" } }, - "node_modules/react-dropzone": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz", - "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==", - "dependencies": { - "attr-accept": "^2.2.2", - "file-selector": "^0.6.0", - "prop-types": "^15.8.1" - }, - "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "react": ">= 16.8 || 18.0.0" - } - }, "node_modules/react-property": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/react-property/-/react-property-2.0.0.tgz", @@ -11202,6 +11406,20 @@ "fs-extra": "^10.0.0" } }, + "node_modules/temp-file/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/terser": { "version": "5.18.2", "resolved": "https://registry.npmjs.org/terser/-/terser-5.18.2.tgz", @@ -11408,6 +11626,59 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -11676,6 +11947,14 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/verror": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", @@ -11740,6 +12019,17 @@ "global": "^4.3.1" } }, + "node_modules/videojs-youtube": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/videojs-youtube/-/videojs-youtube-3.0.1.tgz", + "integrity": "sha512-0gKgag7Zno/dDwIdk+h48ODKDulR4IW62RxGE81PrMwi0OX/wUcKO6m1j+DFYI+7qjtWMZTKnbtQoHGxvUrFQg==", + "dependencies": { + "video.js": "5.x || 6.x || 7.x || 8.x" + }, + "peerDependencies": { + "video.js": "5.x || 6.x || 7.x || 8.x" + } + }, "node_modules/wanakana": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wanakana/-/wanakana-5.1.0.tgz", @@ -12037,6 +12327,17 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 5fbaae6..07cce53 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "name": "miteiru", "description": "Miteiru", - "version": "1.3.0", + "version": "1.4.0", "author": "Hocky Yudhiono ", "main": "app/background.js", "build": { @@ -22,7 +22,7 @@ } }, "scripts": { - "copy": "rsync -r renderer/public/ app/", + "copy": "node copy.js", "predev": "npm run copy", "dev": "nextron", "build": "nextron build", @@ -36,9 +36,12 @@ "@plussub/srt-vtt-parser": "^1.1.0", "ass-compiler": "^0.1.8", "async-await-queue": "^2.1.4", + "axios": "^1.4.0", "detect-file-encoding-and-language": "^2.4.0", "electron-serve": "^1.1.0", "electron-store": "^8.1.0", + "fs-extra": "^11.1.1", + "html-entities": "^2.4.0", "html-react-parser": "^3.0.16", "iconv-lite": "^0.6.3", "jmdict-simplified-node": "^1.1.2", @@ -46,12 +49,12 @@ "patch-package": "^7.0.0", "react-awesome-button": "^7.0.5", "react-colorful": "^5.6.1", - "react-dropzone": "^14.2.3", "react-smooth-collapse": "^2.1.2", "react-video-seek-slider": "^6.0.4", "shunou": "^1.0.2", "styled-components": "^6.0.3", - "video.js": "^8.3.0" + "video.js": "^8.3.0", + "videojs-youtube": "^3.0.1" }, "devDependencies": { "@types/node": "^16.18.38", diff --git a/renderer/components/DataStructures.ts b/renderer/components/DataStructures.ts index ed85c8b..da56efd 100644 --- a/renderer/components/DataStructures.ts +++ b/renderer/components/DataStructures.ts @@ -1,4 +1,3 @@ -import {getFurigana} from "shunou"; import fs from 'fs'; import {parse as parseSRT} from '@plussub/srt-vtt-parser'; import {parse as parseASS} from 'ass-compiler'; @@ -8,6 +7,7 @@ import {ipcRenderer} from "electron"; import {isHiragana, isKatakana, toHiragana} from 'wanakana' import {videoConstants} from "../utils/constants"; import {randomUUID} from "crypto"; +import {Entry} from "@plussub/srt-vtt-parser/dist/src/types"; const languageMap = { @@ -112,13 +112,21 @@ export class SubtitleContainer { const data = parseSRT(text); entries = data.entries; } + this.createFromArrayEntries(subtitleContainer, entries); + return subtitleContainer; + } + + + static createFromArrayEntries(subtitleContainer: SubtitleContainer, entries: Entry[]) { + if (subtitleContainer === null) { + subtitleContainer = new SubtitleContainer(); + } let ans = 0; for (let i = 0; i < Math.min(5, entries.length); i++) { if (entries[i].text.match(videoConstants.cjkRegex)) { ans++; } } - subtitleContainer.language = "EN"; if (ans >= 3) subtitleContainer.language = "JP"; // try { @@ -130,7 +138,7 @@ export class SubtitleContainer { // process transcript entry subtitleContainer.lines.push(new Line(from, to, removeTags(text))) } - return subtitleContainer + return subtitleContainer; } async adjustJapanese(tokenizeMiteiru: (string) => Promise) { @@ -183,4 +191,24 @@ function parseAssSubtitle(text: string) { text: event.Text.combined.replace(/\\N/g, '\n') }; }); -} \ No newline at end of file +} + +interface YoutubeSubtitleEntry { + start: string; + dur: string; + text: string; +} + +export const convertSubtitlesToEntries = (subtitles: YoutubeSubtitleEntry[]): Entry[] => { + const entries: Entry[] = subtitles.map((subtitle, index) => { + const start = Math.round(parseFloat(subtitle.start) * 1000); + const dur = Math.round(parseFloat(subtitle.dur) * 1000); + return { + id: `subtitle-${index}`, + from: start, + to: start + dur, + text: subtitle.text, + }; + }); + return entries; +}; diff --git a/renderer/components/MiteiruDropzone.tsx b/renderer/components/MiteiruDropzone.tsx index ddd37c0..cce70ff 100644 --- a/renderer/components/MiteiruDropzone.tsx +++ b/renderer/components/MiteiruDropzone.tsx @@ -1,47 +1,72 @@ -import React, {useCallback, useState} from "react"; -import {SubtitleContainer} from "./DataStructures"; -import {useDropzone} from "react-dropzone"; -import {Key} from "./KeyboardHelp"; - -const ActiveDropzoneCue = ({isActive}) => { - const message = ["Drag here please UωU", "Drop Here (ᴗ_ ᴗ。)"] - return
-
- -
{message[+isActive]}
-
- or Press to toggle me~! -
-
-
-} +import React, {useEffect, useRef} from "react"; +import {extractVideoId, isYoutube} from "../utils/utils"; export const MiteiruDropzone = ({onDrop}) => { + const dropRef = useRef(null); // Explicitly declaring the type of the ref + + const handleDrag = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + const dt = e.dataTransfer; + const url = dt.getData('text/plain'); + const files = [...dt.files]; // Spread operator to convert FileList to an Array + + if (isYoutube(url)) { + onDrop([{path: url}]); + } else if (files.length) { + const filesWithPath = files.map(file => ({path: file.path})); + onDrop(filesWithPath); + } + }; + + useEffect(() => { + const pasteEvent = () => { + navigator.clipboard.readText().then((clipText) => { + const videoId = extractVideoId(clipText); + if (videoId) { + const path = [{path: clipText}] + onDrop(path) + } + }); + }; - const {getRootProps} = useDropzone({ - noClick: true, - onDrop, - noKeyboard: true, - noDragEventsBubbling: true - }) + window.addEventListener("paste", pasteEvent); + + // Cleanup + return () => { + window.removeEventListener("paste", pasteEvent); + }; + }, [onDrop]); + + useEffect(() => { + const div = dropRef.current; + + if (div) { + div.addEventListener('dragover', handleDrag); + div.addEventListener('drop', handleDrop); + + return () => { + div.removeEventListener('dragover', handleDrag); + div.removeEventListener('drop', handleDrop); + } + } + }, []); // Pass an empty array to ensure that the effect runs only once return ( -
-
) + + ); } -export default MiteiruDropzone; \ No newline at end of file +export default MiteiruDropzone; diff --git a/renderer/components/VideoJS.tsx b/renderer/components/VideoJS.tsx index d54fd6d..da04e9f 100644 --- a/renderer/components/VideoJS.tsx +++ b/renderer/components/VideoJS.tsx @@ -1,6 +1,8 @@ import {useEffect, useRef} from 'react'; import videojs from 'video.js'; import 'video.js/dist/video-js.css'; +import 'videojs-youtube'; // Import the YouTube plugin + export const VideoJS = ({options, onReady, setCurrentTime}) => { const videoRef = useRef(null); @@ -18,7 +20,6 @@ export const VideoJS = ({options, onReady, setCurrentTime}) => { videoRef.current.appendChild(videoElement); const player = playerRef.current = videojs(videoElement, options, () => { - videojs.log('player is ready'); onReady && onReady(player); }); } else { @@ -36,7 +37,7 @@ export const VideoJS = ({options, onReady, setCurrentTime}) => { }, []); return ( -
+
diff --git a/renderer/hooks/useLoadFiles.tsx b/renderer/hooks/useLoadFiles.tsx index f316e11..acee2d5 100644 --- a/renderer/hooks/useLoadFiles.tsx +++ b/renderer/hooks/useLoadFiles.tsx @@ -1,10 +1,15 @@ import {useCallback, useEffect, useState} from 'react'; -import {setGlobalSubtitleId, SubtitleContainer} from "../components/DataStructures"; +import { + convertSubtitlesToEntries, + setGlobalSubtitleId, + SubtitleContainer +} from "../components/DataStructures"; import {randomUUID} from "crypto"; import {TOAST_TIMEOUT} from "../components/Toast"; -import {isSubtitle, isVideo} from "../utils/utils"; +import {extractVideoId, isLocalPath, isSubtitle, isVideo, isYoutube} from "../utils/utils"; import {findPositionDeltaInFolder} from "../utils/folderUtils"; import {useAsyncAwaitQueue} from "./useAsyncAwaitQueue"; +import {ipcRenderer} from 'electron'; const useLoadFiles = (setToastInfo, primarySub, setPrimarySub, secondarySub, setSecondarySub, tokenizeMiteiru, setEnableSeeker, changeTimeTo, player) => { const [videoSrc, setVideoSrc] = useState({src: '', type: '', path: ''}); @@ -16,12 +21,29 @@ const useLoadFiles = (setToastInfo, primarySub, setPrimarySub, secondarySub, set const currentHash = Symbol(); await queue.wait(currentHash); let currentPath = acceptedFiles[0].path; - currentPath = currentPath.replaceAll('\\', '/') - let pathUri = currentPath; - if (process.platform === 'win32') { - pathUri = '/' + currentPath; + let pathUri; + if (isLocalPath(currentPath)) { + currentPath = currentPath.replaceAll('\\', '/') + pathUri = currentPath; + if (process.platform === 'win32') { + pathUri = '/' + currentPath; + } + } + if (isVideo(currentPath) || isYoutube(currentPath)) { + const draggedVideo = isYoutube(currentPath) ? { + type: 'video/youtube', + src: currentPath, + path: currentPath + } : { + type: 'video/webm', + src: `miteiru://${pathUri}`, + path: pathUri + }; + setVideoSrc(draggedVideo); + resetSub(setPrimarySub) + resetSub(setSecondarySub) } - if (isSubtitle(currentPath)) { + if (isSubtitle(currentPath) || isYoutube(currentPath)) { setToastInfo({ message: 'Loading subtitle, please wait!', update: randomUUID() @@ -36,7 +58,8 @@ const useLoadFiles = (setToastInfo, primarySub, setPrimarySub, secondarySub, set type: 'text/plain', src: `${currentPath}` }; - SubtitleContainer.create(draggedSubtitle.src).then(tmpSub => { + const subLoader = (tmpSub, mustMatch = null) => { + if(mustMatch !== null && tmpSub.language !== mustMatch) return; clearInterval(toastSetter); if (tmpSub.language === "JP") { setPrimarySub(tmpSub); @@ -59,20 +82,26 @@ const useLoadFiles = (setToastInfo, primarySub, setPrimarySub, secondarySub, set clearInterval(toastSetter); }) } - }); - } else if (isVideo(currentPath)) { - const draggedVideo = { - type: 'video/webm', - src: `miteiru://${pathUri}`, - path: pathUri }; - setVideoSrc(draggedVideo); - resetSub(setPrimarySub) - resetSub(setSecondarySub) + if (isYoutube(currentPath)) { + ipcRenderer.invoke("getYoutubeSubtitle", extractVideoId(currentPath), "en").then(entries => { + entries = convertSubtitlesToEntries(entries) + const tmpSub = SubtitleContainer.createFromArrayEntries(null, entries) + subLoader(tmpSub, "EN"); + }) + ipcRenderer.invoke("getYoutubeSubtitle", extractVideoId(currentPath), "ja").then(entries => { + entries = convertSubtitlesToEntries(entries) + const tmpSub = SubtitleContainer.createFromArrayEntries(null, entries) + subLoader(tmpSub, "JP"); + }) + } else { + SubtitleContainer.create(draggedSubtitle.src).then(subLoader); + } } await queue.end(currentHash); }, [tokenizeMiteiru]); const onVideoChangeHandler = useCallback(async (delta: number = 1) => { + if (!isLocalPath(videoSrc.path)) return; if (videoSrc.path) { const nextVideo = findPositionDeltaInFolder(videoSrc.path, delta); if (nextVideo !== '') { diff --git a/renderer/pages/video.tsx b/renderer/pages/video.tsx index 15f9d57..db197f8 100644 --- a/renderer/pages/video.tsx +++ b/renderer/pages/video.tsx @@ -74,8 +74,21 @@ function Video() { { export const isVideo = (path) => { return isArrayEndsWithMatcher(path, videoConstants.supportedVideoFormats); } +export const isYoutube = (url) => { + // Regular expression to match YouTube URLs. + // It matches following formats: + // - www.youtube.com/watch?v=VIDEO_ID + // - m.youtube.com/watch?v=VIDEO_ID + // - youtube.com/watch?v=VIDEO_ID + // - www.youtube.com/v/VIDEO_ID + // - http://youtu.be/VIDEO_ID + // - youtube.com/embed/VIDEO_ID + // - https://www.youtube.com/shorts/VIDEO_ID + const pattern = /^(http(s)?:\/\/)?((w){3}.)?youtu(be|.be)?(\.com)?\/(watch|embed|v|shorts)?(\?v=)?(\?embed)?\/?(\S+)?$/; + return pattern.test(url); +} + +export const isDomainUri = (url) => { + try { + const parsedUrl = new URL(url); + return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:'; + } catch (e) { + return false; + } +} + +export const isLocalPath = (url) => { + // If it's not a valid URL, it might be a local path + return !isDomainUri(url); +} + export const isSubtitle = (path) => { return isArrayEndsWithMatcher(path, videoConstants.supportedSubtitleFormats); } +export const extractVideoId = (url) => { + const regex = /(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/(watch\?v=)?([^&]+)/; + const results = regex.exec(url); + if (!results) { + return null; + } + return results[5]; +} + const getFormattedNameFromPath = (path) => { const pathList = path.split('/'); return path ? (' - ' + pathList[pathList.length - 1]) : ''