diff --git a/.github/workflows/test-and-build.yml b/.github/workflows/test-and-build.yml index 3fa87435e5..d4628afeba 100644 --- a/.github/workflows/test-and-build.yml +++ b/.github/workflows/test-and-build.yml @@ -22,5 +22,4 @@ jobs: npm test npm run build --if-present env: - NO_NOTARIZE: true CI: true diff --git a/electron-builder-mas.json b/electron-builder-mas.json new file mode 100644 index 0000000000..f0cb137c0c --- /dev/null +++ b/electron-builder-mas.json @@ -0,0 +1,245 @@ +{ + "appId": "org.splayer.splayerx", + "productName": "SPlayer", + "publish": [ + { + "provider": "github", + "owner": "chiflix", + "repo": "splayerx" + } + ], + "afterPack": "scripts/after-pack.js", + "compression": "maximum", + "directories": { + "output": "build" + }, + "electronVersion": "5.0.11", + "electronDist": "node_modules/@chiflix/electron/dist", + "electronDownload": { + "mirror": "https://github.com/chiflix/electron/releases/download/v", + "isVerifyChecksum": false, + "version": "5.0.11" + }, + "files": [ + "dist/electron/**/*" + ], + "mac": { + "hardenedRuntime": false, + "asarUnpack": [ + "node_modules/osx-mouse-cocoa" + ], + "icon": "icons/icon.icns", + "fileAssociations": [ + { + "name": "Video", + "ext": [ + "3g2", + "3gp", + "3gp2", + "3gpp", + "amv", + "asf", + "bik", + "bin", + "crf", + "divx", + "drc", + "dv", + "dvr-ms", + "evo", + "gvi", + "gxf", + "m1v", + "m2v", + "m2t", + "m2ts", + "mp2", + "mp2v", + "mp4v", + "mpe", + "mpeg", + "mpeg1", + "mpeg2", + "mpeg4", + "mpv2", + "mts", + "mtv", + "mxf", + "mxg", + "nsv", + "nuv", + "ogg", + "ogm", + "ogv", + "ogx", + "ps", + "rec", + "rpl", + "thp", + "tod", + "tp", + "tts", + "txd", + "vro", + "wm", + "wtv", + "xesc" + ], + "role": "Viewer", + "icon": "build/icons/others.icns" + }, + { + "name": "DAT", + "ext": [ + "dat" + ], + "role": "Viewer", + "icon": "build/icons/dat.icns" + }, + { + "name": "WEBM", + "ext": [ + "webm" + ], + "role": "Viewer", + "icon": "build/icons/webm.icns" + }, + { + "name": "VOB", + "ext": [ + "vob" + ], + "role": "Viewer", + "icon": "build/icons/vob.icns" + }, + { + "name": "TS", + "ext": [ + "ts" + ], + "role": "Viewer", + "icon": "build/icons/ts.icns" + }, + { + "name": "RM", + "ext": [ + "rm" + ], + "role": "Viewer", + "icon": "build/icons/rm.icns" + }, + { + "name": "MPG", + "ext": [ + "mpg" + ], + "role": "Viewer", + "icon": "build/icons/mpg.icns" + }, + { + "name": "F4V", + "ext": [ + "f4v" + ], + "role": "Viewer", + "icon": "build/icons/f4v.icns" + }, + { + "name": "AVI", + "ext": [ + "avi" + ], + "role": "Viewer", + "icon": "build/icons/avi.icns" + }, + { + "name": "FLV", + "ext": [ + "flv" + ], + "role": "Viewer", + "icon": "build/icons/flv.icns" + }, + { + "name": "M4V", + "ext": [ + "m4v" + ], + "role": "Viewer", + "icon": "build/icons/m4v.icns" + }, + { + "name": "MKV", + "ext": [ + "mkv" + ], + "role": "Viewer", + "icon": "build/icons/mkv.icns" + }, + { + "name": "MOV", + "ext": [ + "mov" + ], + "role": "Viewer", + "icon": "build/icons/mov.icns" + }, + { + "name": "MP4", + "ext": [ + "mp4" + ], + "role": "Viewer", + "icon": "build/icons/mp4.icns" + }, + { + "name": "RMVB", + "ext": [ + "rmvb" + ], + "role": "Viewer", + "icon": "build/icons/rmvb.icns" + }, + { + "name": "WMV", + "ext": [ + "wmv" + ], + "role": "Viewer", + "icon": "build/icons/wmv.icns" + }, + { + "name": "SRT", + "ext": [ + "srt" + ], + "role": "Viewer", + "icon": "build/icons/srt.icns" + }, + { + "name": "ASS", + "ext": [ + "ass" + ], + "role": "Viewer", + "icon": "build/icons/ass.icns" + }, + { + "name": "VTT", + "ext": [ + "vtt" + ], + "role": "Viewer", + "icon": "build/icons/vtt.icns" + }, + { + "name": "SSA", + "ext": [ + "ssa" + ], + "role": "Viewer", + "icon": "build/icons/ssa.icns" + } + ] + } +} diff --git a/electron-builder.json b/electron-builder.json index 3104e585f6..04ef5cc005 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -58,7 +58,7 @@ ] }, "mac": { - "hardenedRuntime": false, + "hardenedRuntime": true, "icon": "icons/icon.icns", "target": [ "dmg" diff --git a/package.json b/package.json index d1348d9610..d4a3b78370 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,13 @@ "node": "^12" }, "scripts": { - "build": "node .electron-vue/build.js && electron-builder --p never", - "build:appx": "cross-env ENVIRONMENT_NAME=APPX node .electron-vue/build.js && electron-builder --p never -w appx", + "build": "node .electron-vue/build.js && electron-builder -p never", + "build:appx": "cross-env ENVIRONMENT_NAME=APPX node .electron-vue/build.js && electron-builder -p never -w appx", "build:mas": "scripts/mas-build.sh mas", "build:mas-dev": "scripts/mas-build.sh mas-dev", "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", "start": "npm run stage", - "dev": "cross-env SAGI_API=sagi-api:8443 ACCOUNT_API=http://dev.account.splayer.work node .electron-vue/dev-runner.js", + "dev": "cross-env SAGI_API=sagi-api:8443 node .electron-vue/dev-runner.js", "stage": "node .electron-vue/dev-runner.js", "install-app-deps": "electron-builder install-app-deps", "lint": "eslint --ext .ts,.js,.vue -f ./node_modules/eslint-friendly-formatter src test", @@ -38,13 +38,8 @@ "@sentry/electron": "^0.17.1", "@sentry/integrations": "^5.1.2", "@tensorflow/tfjs": "^1.2.2", - "@types/chardet": "^0.5.0", - "@types/franc": "^4.0.0", - "@types/fs-extra": "^7.0.0", - "@types/lodash": "^4.14.134", - "@types/lolex": "^3.1.1", + "abort-controller": "^3.0.0", "ass-compiler": "github:YvonTre/ass-compiler", - "axios": "^0.19.0", "chardet": "^0.7.0", "configcat-js": "^1.1.19", "electron-json-storage": "github:ipy/electron-json-storage", @@ -63,6 +58,7 @@ "lolex": "^3.0.0", "lottie-web": "^5.5.4", "mkdirp": "^0.5.1", + "node-fetch": "^2.6.0", "nzh": "^1.0.4", "os-locale": "^3.0.1", "p-queue": "^6.0.0", @@ -77,7 +73,6 @@ "vue": "^2.6.10", "vue-analytics": "^5.16.1", "vue-async-computed": "^3.5.1", - "vue-axios": "^2.1.4", "vue-devtools": "5.1.0", "vue-electron": "^1.0.6", "vue-electron-json-storage": "^1.0.1", @@ -114,6 +109,11 @@ "@chiflix/electron": "5.0.11", "@sentry/webpack-plugin": "^1.6.2", "@types/chai": "^4.1.7", + "@types/chardet": "^0.5.0", + "@types/franc": "^4.0.0", + "@types/fs-extra": "^7.0.0", + "@types/lodash": "^4.14.134", + "@types/lolex": "^3.1.1", "@types/mkdirp": "^0.5.2", "@types/mocha": "^5.2.7", "@types/node": "^12.7.4", diff --git a/scripts/mas-build.sh b/scripts/mas-build.sh index be869f8ee5..41248c438b 100755 --- a/scripts/mas-build.sh +++ b/scripts/mas-build.sh @@ -3,14 +3,14 @@ # Reinstall the electron mas version ELECTRON_VERSION=`node -p -e "require('./package.json').devDependencies['@chiflix/electron']"` ELECTRON_VERSION=${ELECTRON_VERSION/^/''} -force_no_cache='true' npm_config_platform=mas npm i @chiflix/electron@$ELECTRON_VERSION +force_no_cache='true' npm_config_platform=mas npm i @chiflix/electron@$ELECTRON_VERSION --save-exact node .electron-vue/build.js rev=`git rev-list --count HEAD` -electron-builder --p never -m $1 \ - -c electron-builder.json \ +electron-builder -p never -m $1 \ + -c electron-builder-mas.json \ -c.mac.provisioningProfile="build/$1.provisionprofile" \ -c.mac.bundleVersion="$rev" \ -c.mac.category="public.app-category.entertainment" diff --git a/scripts/notarize.js b/scripts/notarize.js index d8e448ae92..b492639004 100644 --- a/scripts/notarize.js +++ b/scripts/notarize.js @@ -2,10 +2,10 @@ require('dotenv').config(); const { notarize } = require('electron-notarize'); exports.default = async function notarizing(context) { - if (process.env.NO_NOTARIZE) return; + if (!process.env.APPLEIDPASS) return; const { electronPlatformName, appOutDir } = context; - if (electronPlatformName !== 'darwin' && electronPlatformName !== 'mas') { + if (electronPlatformName !== 'darwin') { // dmg only return; } diff --git a/src/main/helpers/AudioGrabService.ts b/src/main/helpers/AudioGrabService.ts index 4c3fd93fe8..618e43d286 100644 --- a/src/main/helpers/AudioGrabService.ts +++ b/src/main/helpers/AudioGrabService.ts @@ -10,7 +10,6 @@ import { EventEmitter } from 'events'; import { splayerx } from 'electron'; import path from 'path'; import fs from 'fs'; -import axios from 'axios'; import { credentials, Metadata } from 'grpc'; import { TranslationClient } from 'sagi-api/translation/v1/translation_grpc_pb'; @@ -20,6 +19,7 @@ import { StreamingTranslationRequestConfig, } from 'sagi-api/translation/v1/translation_pb'; import { IAudioStream } from '@/plugins/mediaTasks/mediaInfoQueue'; +import { getIP } from '../../shared/utils'; type JobData = { videoSrc: string, @@ -199,14 +199,12 @@ export default class AudioGrabService extends EventEmitter { metadata.set('uuid', uuid); metadata.set('agent', agent); if (token) { - metadata.set('Authorization', token); + metadata.set('token', token); } - console.log(metadata); // eslint-disable-line - // eslint-disable-next-line @typescript-eslint/no-explicit-any - axios.get('https://ip.xindong.com/myip', { responseType: 'text' }).then((response: any) => { - metadata.set('clientip', response.data); - cb(null, metadata); - }, () => { + getIP().then((ip) => { + metadata.set('clientip', ip); + }).finally(() => { + console.log(metadata); // eslint-disable-line cb(null, metadata); }); }; diff --git a/src/main/index.js b/src/main/index.js index bad323edb8..bf09608ab5 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -15,7 +15,7 @@ import { audioGrabService } from './helpers/AudioGrabService'; import './helpers/electronPrototypes'; import writeLog from './helpers/writeLog'; import { - getValidVideoRegex, getValidSubtitleRegex, getIP, getToken, saveToken, + getValidVideoRegex, getValidSubtitleRegex, getToken, saveToken, } from '../shared/utils'; import { mouse } from './helpers/mouse'; import MenuService from './menu/MenuService'; @@ -89,7 +89,6 @@ let needBlockCloseLaborWindow = true; // 标记是否阻塞nsfw窗口关闭 let inited = false; let hideBrowsingWindow = false; let finalVideoToOpen = []; -let ip = ''; // 本机ip地址 const locale = new Locale(); const tmpVideoToOpen = []; const tmpSubsToOpen = []; @@ -1477,13 +1476,3 @@ app.on('sign-out', () => { mainWindow.webContents.send('sign-in', undefined); } }); - -app.getIP = async () => { - if (ip) return ip; - try { - ip = await getIP(); - } catch (error) { - // empty - } - return ip; -}; diff --git a/src/renderer/containers/LandingView.vue b/src/renderer/containers/LandingView.vue index 27daaaa628..20a73d50e7 100644 --- a/src/renderer/containers/LandingView.vue +++ b/src/renderer/containers/LandingView.vue @@ -312,9 +312,9 @@ export default { this.$electron.ipcRenderer.send('callMainWindowMethod', 'setMinimumSize', [720, 405]); this.$electron.ipcRenderer.send('callMainWindowMethod', 'setAspectRatio', [720 / 405]); - Sagi.healthCheck().then((status) => { + Sagi.healthCheck().then((res) => { if (process.env.NODE_ENV !== 'production') { - this.sagiHealthStatus = status; + this.sagiHealthStatus = res.status; log.info('LandingView.vue', `launching: ${app.getName()} ${app.getVersion()}`); } }); diff --git a/src/renderer/containers/Login/SMS.vue b/src/renderer/containers/Login/SMS.vue index 895d647989..148a490360 100644 --- a/src/renderer/containers/Login/SMS.vue +++ b/src/renderer/containers/Login/SMS.vue @@ -61,6 +61,7 @@ import { remote } from 'electron'; import { parsePhoneNumberFromString, getCountryCallingCode } from 'libphonenumber-js/mobile'; import { log } from '@/libs/Log'; import { signIn, getSMSCode } from '@/libs/apis'; +import { getIP } from '@/../shared/utils'; export default Vue.extend({ name: 'SMS', @@ -94,7 +95,7 @@ export default Vue.extend({ }, async mounted() { // @ts-ignore - const ip = await remote.app.getIP(); + const ip = await getIP(); log.debug('ip', ip); const geo = geoip.lookup(ip); if (geo && geo.country && getCountryCallingCode(geo.country)) { @@ -178,7 +179,7 @@ export default Vue.extend({ await signIn('code', `+${this.countryCallCode}${this.mobile}`, this.code); window.close(); } catch (error) { - if (error.code === '400') { + if (error.status === 400) { this.message = this.$t('loginModal.codeError'); } else { this.message = this.$t('loginModal.netWorkError'); diff --git a/src/renderer/helpers/featureSwitch.ts b/src/renderer/helpers/featureSwitch.ts index 556e3d0810..ee4b06e29e 100644 --- a/src/renderer/helpers/featureSwitch.ts +++ b/src/renderer/helpers/featureSwitch.ts @@ -1,7 +1,8 @@ -import Vue from 'vue'; import * as configcat from 'configcat-js'; +import store from '@/store'; import { log } from '@/libs/Log'; import { getMainVersion } from '@/libs/utils'; +import { getSystemLocale, getClientUUID } from '@/../shared/utils'; const configCatApiKey = process.env.NODE_ENV === 'development' ? 'WizXCIVndyJUn4cCRD3qvQ/8uwWLI_KhUmuOrOaDDsaxQ' @@ -11,39 +12,24 @@ const client = configcat.createClientWithLazyLoad(configCatApiKey, { cacheTimeToLiveSeconds: 600, }); -function getUserId() { - try { - return Vue.axios.defaults.headers.common['X-Application-Token'] || ''; - } catch (ex) { - return ''; - } -} - -function getApplicationDisplayLanguage() { - try { - return Vue.axios.defaults.headers.common['X-Application-Display-Language'] || ''; - } catch (ex) { - return ''; - } -} - -function getUserObject() { +async function getUserObject() { return { - identifier: getUserId(), + identifier: await getClientUUID(), custom: { version: getMainVersion(), - displayLanguage: getApplicationDisplayLanguage(), + displayLanguage: store.getters.displayLanguage || getSystemLocale(), }, }; } export async function getConfig(configKey: string, defaultValue?: T): Promise { - log.debug('configKey', getUserObject()); + const userObject = await getUserObject(); + log.debug('configKey', userObject); return new Promise((resolve) => { setTimeout(() => resolve(defaultValue), 10000); client.getValue(configKey, defaultValue, (value: T) => { resolve(value); - }, getUserObject()); + }, userObject); }); } diff --git a/src/renderer/libs/apis.ts b/src/renderer/libs/apis.ts index cfe03be7a3..ca0db2147c 100644 --- a/src/renderer/libs/apis.ts +++ b/src/renderer/libs/apis.ts @@ -1,11 +1,17 @@ -import axios, { AxiosError, AxiosResponse } from 'axios'; import { remote } from 'electron'; import { log } from '@/libs/Log'; import { apiOfAccountService } from '@/helpers/featureSwitch'; +import Fetcher from '@/../shared/Fetcher'; -const instance = axios.create(); +export class ApiError extends Error { + /** HTTP status */ + public status: number; -instance.defaults.timeout = 1000 * 10; + /** Message from server */ + public message: string; +} + +const fetcher = new Fetcher(); async function getEndpoint() { const api = await apiOfAccountService(); @@ -14,84 +20,60 @@ async function getEndpoint() { } export function setToken(t: string) { - instance.defaults.headers.common['Authorization'] = `Bearer ${t}`; + fetcher.setHeader('Authorization', `Bearer ${t}`); } -function intercept(response: AxiosResponse) { - const headers = response.headers; - if (headers && headers.authorization) { - const token = headers.authorization.replace('Bearer', '').trim(); +fetcher.useResponseInterceptor((res) => { + const authorization = res.headers.get('Authorization'); + if (authorization) { + const token = authorization.replace('Bearer', '').trim(); let displayName = ''; try { - displayName = JSON.parse(new Buffer(token.split('.')[1], 'base64').toString()).display_name; // eslint-disable-line + displayName = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).display_name; // eslint-disable-line + log.debug('apis/account/token', token); + remote.app.emit('sign-in', { + token, + displayName, + }); } catch (error) { - // tmpty + log.error('apis/account/token', token); } - log.debug('token', token); - remote.app.emit('sign-in', { - token, - displayName, - }); } -} - -instance.interceptors.response.use((response: AxiosResponse) => { - intercept(response); - return response; -}, (error: AxiosError) => Promise.reject(error)); + return res; +}); -export function checkToken() { - return new Promise(async (resolve, reject) => { - const endpoint = await getEndpoint(); - instance.get(`${endpoint}/auth/check`) - .then((response: AxiosResponse) => { - log.debug('checkToken', response); - resolve(true); - }) - .catch((error: AxiosError) => { - reject(error); - }); - }); +export async function checkToken() { + const endpoint = await getEndpoint(); + const res = await fetcher.get(`${endpoint}/auth/check`); + log.debug('api/account/checkToken', res); + return res.ok; } -export function getSMSCode(phone: string) { - return new Promise(async (resolve, reject) => { - const endpoint = await getEndpoint(); - log.debug('sms', `${endpoint}/auth/sms`); - instance.post(`${endpoint}/auth/sms`, { - phone, - }) - .then((response: AxiosResponse) => { - log.debug('sms', response); - resolve(true); - }) - .catch((error: AxiosError) => { - log.debug('sms', error); - reject(error); - }); - }); +export async function getSMSCode(phone: string) { + const endpoint = await getEndpoint(); + const res = await fetcher.post(`${endpoint}/auth/sms`, { phone }); + log.debug('api/account/sms', res); + return res.ok; } -export function signIn(type: string, phone: string, code?: string) { - return new Promise(async (resolve, reject) => { - const endpoint = await getEndpoint(); - instance.post(`${endpoint}/auth/login`, { - phone, - type, - code, - }) - .then((response: AxiosResponse) => { - resolve(response.data); - }) - .catch((error: AxiosError) => { - if (error && error.response && error.response.status === 400) { - error.code = '400'; - reject(error); - } else { - reject(error); - } - }); - }); +export async function signIn(type: string, phone: string, code: string) { + const endpoint = await getEndpoint(); + const res = await fetcher.post(`${endpoint}/auth/login`, { phone, type, code }); + if (res.ok) { + const data = await res.json(); + log.debug('api/account/login', data); + return data; + } + const error = new ApiError(); + error.status = res.status; + // Server message is not localized yet + // try { + // const data = await res.json(); + // error.message = data.message; + // } catch (ex) { + // error.message = ''; + // } + throw error; } export function signOut() { diff --git a/src/renderer/libs/sagi.ts b/src/renderer/libs/sagi.ts index c8a84eb002..9796e60894 100644 --- a/src/renderer/libs/sagi.ts +++ b/src/renderer/libs/sagi.ts @@ -1,7 +1,6 @@ import path from 'path'; import fs from 'fs'; import grpc, { credentials, Metadata } from 'grpc'; -import Vue from 'vue'; import { HealthCheckRequest, HealthCheckResponse } from 'sagi-api/health/v1/health_pb'; import { HealthClient } from 'sagi-api/health/v1/health_grpc_pb'; import { @@ -17,6 +16,7 @@ import { TranslationClient } from 'sagi-api/translation/v1/translation_grpc_pb'; import { TrainingData } from 'sagi-api/training/v1/training_pb'; import { TrainngClient } from 'sagi-api/training/v1/training_grpc_pb'; import { SagiSubtitlePayload } from '@/services/subtitle'; +import { getClientUUID, getIP } from '@/../shared/utils'; import { log } from './Log'; export class Sagi { @@ -26,14 +26,11 @@ export class Sagi { private creds: grpc.ChannelCredentials; - private ip: string; - public constructor() { this.creds = this.combinedCreds(); } private combinedCreds(token?: string) { - const { ip } = this; const sslCreds = credentials.createSsl( // How to access resources with fs see: // https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html @@ -42,24 +39,16 @@ export class Sagi { fs.readFileSync(path.join(__static, '/certs/cert.pem')), ); const metadataUpdater = (_: unknown, cb: Function) => { - const metadata = new Metadata(); - metadata.set('uuid', Vue.axios.defaults.headers.common['X-Application-Token']); - metadata.set('agent', navigator.userAgent); - if (token) { - metadata.set('token', token); - } - if (ip) { + Promise.all([getClientUUID(), getIP()]).then(([uuid, ip]) => { + const metadata = new Metadata(); + metadata.set('uuid', uuid); + metadata.set('agent', navigator.userAgent); metadata.set('clientip', ip); - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Vue.axios.get('https://ip.xindong.com/myip', { responseType: 'text' }).then((response: any) => { - metadata.set('clientip', response.bodyText); - this.ip = response.bodyText; - cb(null, metadata); - }, () => { - cb(null, metadata); - }); - } + if (token) { + metadata.set('token', token); + } + cb(null, metadata); + }); }; const metadataCreds = credentials.createFromMetadataGenerator(metadataUpdater); return credentials.combineChannelCredentials(sslCreds, metadataCreds); diff --git a/src/renderer/libs/utils.ts b/src/renderer/libs/utils.ts index 9f93b7d658..f992db9db8 100644 --- a/src/renderer/libs/utils.ts +++ b/src/renderer/libs/utils.ts @@ -5,7 +5,6 @@ import { times, padStart, sortBy } from 'lodash'; import { sep, basename, join } from 'path'; import { ensureDir } from 'fs-extra'; import { remote } from 'electron'; -import axios, { AxiosResponse } from 'axios'; // @ts-ignore import { promises as fsPromises } from 'fs'; // @ts-ignore @@ -20,6 +19,7 @@ import { import { codeToLanguageName, LanguageCode } from './language'; import { checkPathExist, write, deleteDir } from './file'; import { IEmbeddedOrigin } from '@/services/subtitle/utils/loaders'; +import Fetcher from '@/../shared/Fetcher'; import { isBetaVersion } from '../../shared/common/platform'; /** @@ -403,30 +403,30 @@ export function compareVersions(left: string, right: string): boolean { * @param {boolean} auto is auto check for updates * @returns {Promise} example { version: "4.2.2", isLastest: true } */ -export function checkForUpdate( +export async function checkForUpdate( auto: boolean, ): Promise<{ version: string, isLastest: boolean, landingPage: string, url: string }> { const skipVersion = localStorage.getItem('skip-check-for-update'); const url = isBetaVersion ? 'https://beta.splayer.org/beta/latest.json' : 'https://www.splayer.org/stable/latest.json'; - return axios.get(url, { timeout: 10000 }) - .then((res: AxiosResponse) => { // eslint-disable-line complexity - const result = { - version, - isLastest: true, - landingPage: '', - url: '', - }; - // check package.json.version with res.data - if (res.data && res.data.name !== version && !(res.data.name === skipVersion && auto) - && compareVersions(version, res.data.name)) { - result.version = res.data.name; - result.isLastest = false; - result.landingPage = res.data.landingPage; - result.url = res.data.files[process.platform].url; - } - return result; - }); + const fetcher = new Fetcher(); + const res = await fetcher.fetch(url); + const data = await res.json(); + const result = { + version, + isLastest: true, + landingPage: '', + url: '', + }; + // check package.json.version with data + if (data && data.name !== version && !(data.name === skipVersion && auto) + && compareVersions(version, data.name)) { + result.version = data.name; + result.isLastest = false; + result.landingPage = data.landingPage; + result.url = data.files[process.platform].url; + } + return result; } /** diff --git a/src/renderer/login.ts b/src/renderer/login.ts index 798ce6af26..ac5400df9e 100644 --- a/src/renderer/login.ts +++ b/src/renderer/login.ts @@ -1,9 +1,7 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import axios from 'axios'; import VueRouter from 'vue-router'; import VueI18n from 'vue-i18n'; -import VueAxios from 'vue-axios'; import electron from 'electron'; import osLocale from 'os-locale'; import { hookVue } from '@/kerning'; @@ -16,7 +14,6 @@ import '@/css/style.scss'; Vue.use(VueI18n); Vue.use(Vuex); Vue.use(VueRouter); -Vue.use(VueAxios, axios); function getSystemLocale() { const { app } = electron.remote; diff --git a/src/renderer/main.ts b/src/renderer/main.ts index 1f2a2f0614..ab2b57e46d 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -7,15 +7,10 @@ import fs from 'fs'; import electron, { ipcRenderer } from 'electron'; import Vue from 'vue'; import VueI18n from 'vue-i18n'; -import axios from 'axios'; import { mapGetters, mapActions, createNamespacedHelpers } from 'vuex'; -import uuidv4 from 'uuid/v4'; import osLocale from 'os-locale'; -import VueAxios from 'vue-axios'; import { throttle } from 'lodash'; // @ts-ignore -import VueElectronJSONStorage from 'vue-electron-json-storage'; -// @ts-ignore import VueAnalytics from 'vue-analytics'; // @ts-ignore import VueElectron from 'vue-electron'; @@ -49,7 +44,7 @@ import { SNAPSHOT_FAILED, SNAPSHOT_SUCCESS, LOAD_SUBVIDEO_FAILED } from './helpe import InputPlugin, { getterTypes as iGT } from '@/plugins/input'; import { VueDevtools } from './plugins/vueDevtools.dev'; import { ISubtitleControlListItem, Type, NOT_SELECTED_SUBTITLE } from './interfaces/ISubtitle'; -import { getValidSubtitleRegex } from '../shared/utils'; +import { getValidSubtitleRegex, getSystemLocale, getClientUUID } from '../shared/utils'; import { isWindowsExE, isMacintoshDMG } from '../shared/common/platform'; import MenuService from './services/menu/MenuService'; @@ -57,19 +52,6 @@ import MenuService from './services/menu/MenuService'; // causing callbacks-registry.js 404 error. disable temporarily // require('source-map-support').install(); -function getSystemLocale() { - const { app } = electron.remote; - let locale = process.platform === 'win32' ? app.getLocale() : osLocale.sync(); - locale = locale.replace('_', '-'); - if (locale === 'zh-TW' || locale === 'zh-HK' || locale === 'zh-Hant') { - return 'zh-Hant'; - } - if (locale.startsWith('zh')) { - return 'zh-Hans'; - } - return 'en'; -} - function getEnvironmentName() { if (process.platform === 'darwin') { return process.mas ? 'MAS' : 'DMG'; @@ -115,8 +97,6 @@ Vue.directive('fade-in', { Vue.use(VueElectron); Vue.use(VueI18n); -Vue.use(VueElectronJSONStorage); -Vue.use(VueAxios, axios); Vue.use(AsyncComputed); Vue.use(VueAnalytics, { id: (process.env.NODE_ENV === 'production') ? 'UA-2468227-6' : 'UA-2468227-5', @@ -422,19 +402,8 @@ new Vue({ this.$bus.$on('new-file-open', () => { this.menuService.addRecentPlayItems(); }); - // TODO: Setup user identity - this.$storage.get('user-uuid', (err: Error, userUUID: string) => { - if (err || Object.keys(userUUID).length === 0) { - err && log.error('render/main', err); - userUUID = uuidv4(); - this.$storage.set('user-uuid', userUUID); - } - - Vue.axios.defaults.headers.common['X-Application-Token'] = userUUID; - Vue.axios.defaults.headers.common['X-Application-Display-Language'] = this.displayLanguage; - - // set userUUID to google analytics uid - this.$ga && this.$ga.set('userId', userUUID); + getClientUUID().then((clientId: string) => { + this.$ga && this.$ga.set('userId', clientId); }); this.$on('wheel-event', this.wheelEventHandler); diff --git a/src/renderer/services/media/AudioTranslateService.ts b/src/renderer/services/media/AudioTranslateService.ts index f73e6a7281..dff20c434c 100644 --- a/src/renderer/services/media/AudioTranslateService.ts +++ b/src/renderer/services/media/AudioTranslateService.ts @@ -9,7 +9,6 @@ import { ipcRenderer, Event } from 'electron'; import { EventEmitter } from 'events'; import { isNaN } from 'lodash'; -import Vue from 'vue'; import { StreamingTranslationResponse, } from 'sagi-api/translation/v1/translation_pb'; @@ -19,6 +18,7 @@ import MediaStorageService, { mediaStorageService } from '../storage/MediaStorag import { TranscriptInfo } from '../subtitle'; import { Stream } from '@/plugins/mediaTasks/mediaInfoQueue'; import { isAccountEnabled } from '@/helpers/featureSwitch'; +import { getClientUUID } from '@/../shared/utils'; type JobData = { audioId: string, @@ -105,15 +105,17 @@ class AudioTranslateService extends EventEmitter { this.audioLanguageCode = data.audioLanguageCode; this.targetLanguageCode = data.targetLanguageCode; this.audioInfo = data.audioInfo; - ipcRenderer.send('grab-audio', { - mediaHash: this.mediaHash, - videoSrc: this.videoSrc, - audioLanguageCode: this.audioLanguageCode, - targetLanguageCode: this.targetLanguageCode, - audioId: this.audioId, - audioInfo: this.audioInfo, - uuid: Vue.axios.defaults.headers.common['X-Application-Token'], - agent: navigator.userAgent, + getClientUUID().then((uuid: string) => { + ipcRenderer.send('grab-audio', { + mediaHash: this.mediaHash, + videoSrc: this.videoSrc, + audioLanguageCode: this.audioLanguageCode, + targetLanguageCode: this.targetLanguageCode, + audioId: this.audioId, + audioInfo: this.audioInfo, + uuid, + agent: navigator.userAgent, + }); }); ipcRenderer.removeListener('grab-audio-update', this.ipcCallBack); ipcRenderer.on('grab-audio-update', this.ipcCallBack); diff --git a/src/renderer/typings.d.ts b/src/renderer/typings.d.ts index f6e5a4be2f..0a43c5462f 100644 --- a/src/renderer/typings.d.ts +++ b/src/renderer/typings.d.ts @@ -2,7 +2,6 @@ /* eslint-disable @typescript-eslint/interface-name-prefix */ // eslint-disable-next-line @typescript-eslint/no-unused-vars import Vue, { VNode } from 'vue'; // eslint-disable-line no-unused-vars -import { AxiosInstance } from 'axios'; declare global { declare const __static: string; //eslint-disable-line @@ -18,9 +17,16 @@ declare global { } } - interface JsonMap { [member: string]: string | number | boolean | null | JsonArray | JsonMap } + type Optional = { [key in keyof T]?: T[key] }; + interface JsonMap { + [member: string]: string | number | boolean | null | JsonArray | JsonMap; + } interface JsonArray extends Array {} // eslint-disable-line type Json = JsonMap | JsonArray | string | number | boolean | null; + + interface AbortablePromise extends Promise { + abort(); + } } declare module '*.vue' { @@ -33,12 +39,6 @@ declare module 'vue/types/vue' { $bus: Vue; $ga: any; $electron: Electron.RendererInterface; - axios: AxiosInstance; - $http: AxiosInstance; - } - - namespace Vue { - const axios: AxiosInstance; } } @@ -175,77 +175,147 @@ declare module 'electron' { interface IpcMain { on(channel: 'media-info-request', listener: (event: Event, path: string) => void): this; - on(channel: 'snapshot-request', listener: (event: Event, - videoPath: string, imagePath: string, - timeString: string, - width: number, height: number, - ) => void): this; - on(channel: 'subtitle-metadata-request', listener: (event: Event, - videoPath: string, streamIndex: number, subtitlePath: string, - ) => void): this; - on(channel: 'subtitle-cache-request', listener: (event: Event, - videoPath: string, streamIndex: number - ) => void): this; - on(channel: 'subtitle-stream-request', listener: (event: Event, - videoPath: string, streamIndex: number, time: number, - ) => void): this; - on(channel: 'subtitle-destroy-request', listener: (event: Event, - videoPath: string, streamIndex: number, - ) => void): this; - on(channel: 'thumbnail-request', listener: (event: Event, - videoPath: string, imagePath: string, - thumbnailWidth: number, - columnThumbnailCount: number, rowThumbnailCount: number, - ) => void): this; + on( + channel: 'snapshot-request', + listener: ( + event: Event, + videoPath: string, + imagePath: string, + timeString: string, + width: number, + height: number, + ) => void, + ): this; + on( + channel: 'subtitle-metadata-request', + listener: ( + event: Event, + videoPath: string, + streamIndex: number, + subtitlePath: string, + ) => void, + ): this; + on( + channel: 'subtitle-cache-request', + listener: (event: Event, videoPath: string, streamIndex: number) => void, + ): this; + on( + channel: 'subtitle-stream-request', + listener: (event: Event, videoPath: string, streamIndex: number, time: number) => void, + ): this; + on( + channel: 'subtitle-destroy-request', + listener: (event: Event, videoPath: string, streamIndex: number) => void, + ): this; + on( + channel: 'thumbnail-request', + listener: ( + event: Event, + videoPath: string, + imagePath: string, + thumbnailWidth: number, + columnThumbnailCount: number, + rowThumbnailCount: number, + ) => void, + ): this; } interface IpcRenderer { send(channel: 'media-info-request', path: string): void; - send(channel: 'snapshot-request', - videoPath: string, imagePath: string, + send( + channel: 'snapshot-request', + videoPath: string, + imagePath: string, timeString: string, - width: number, height: number, + width: number, + height: number, ): void; - send(channel: 'subtitle-metadata-request', - videoPath: string, streamIndex: number, subtitlePath: string, - ): this; - send(channel: 'subtitle-cache-request', - videoPath: string, streamIndex: number - ): this; - send(channel: 'subtitle-stream-request', - videoPath: string, streamIndex: number, time: number, + send( + channel: 'subtitle-metadata-request', + videoPath: string, + streamIndex: number, + subtitlePath: string, ): this; - send(channel: 'subtitle-destroy-request', - videoPath: string, streamIndex: number + send(channel: 'subtitle-cache-request', videoPath: string, streamIndex: number): this; + send( + channel: 'subtitle-stream-request', + videoPath: string, + streamIndex: number, + time: number, ): this; - send(channel: 'thumbnail-request', - videoPath: string, imagePath: string, + send(channel: 'subtitle-destroy-request', videoPath: string, streamIndex: number): this; + send( + channel: 'thumbnail-request', + videoPath: string, + imagePath: string, thumbnailWidth: number, - columnThumbnailCount: number, rowThumbnailCount: number, + columnThumbnailCount: number, + rowThumbnailCount: number, ): void; - on(channel: 'media-info-reply', listener: (event: Event, error?: Error, info: string) => void): this; - on(channel: 'snapshot-reply', listener: (event: Event, error?: Error, path: string) => void): this; - on(channel: 'subtitle-metadata-reply', listener: (event: Event, error?: Error, finished: boolean, matadata?: string) => void): this; - on(channel: 'subtitle-cache-reply', listener: (event: Event, error?: Error, finished: boolean, payload?: string) => void): this; - on(channel: 'subtitle-stream-reply', listener: (event: Event, error?: Error, dialogue: string) => void): this; + on( + channel: 'media-info-reply', + listener: (event: Event, error?: Error, info: string) => void, + ): this; + on( + channel: 'snapshot-reply', + listener: (event: Event, error?: Error, path: string) => void, + ): this; + on( + channel: 'subtitle-metadata-reply', + listener: (event: Event, error?: Error, finished: boolean, matadata?: string) => void, + ): this; + on( + channel: 'subtitle-cache-reply', + listener: (event: Event, error?: Error, finished: boolean, payload?: string) => void, + ): this; + on( + channel: 'subtitle-stream-reply', + listener: (event: Event, error?: Error, dialogue: string) => void, + ): this; on(channel: 'subtitle-destroy-reply', listener: (event: Event, error?: Error) => void): this; - on(channel: 'thumbnail-reply', listener: (event: Event, error?: Error, path: string) => void): this; + on( + channel: 'thumbnail-reply', + listener: (event: Event, error?: Error, path: string) => void, + ): this; - once(channel: 'media-info-reply', listener: (event: Event, error?: Error, info: string) => void): this; - once(channel: 'snapshot-reply', listener: (event: Event, error?: Error, path: string) => void): this; - once(channel: 'subtitle-metadata-reply', listener: (event: Event, error?: Error, finished: boolean, matadata?: string) => void): this; - once(channel: 'subtitle-cache-reply', listener: (event: Event, error?: Error, finished: boolean, payload?: string) => void): this; - once(channel: 'subtitle-stream-reply', listener: (event: Event, error?: Error, dialogue: string) => void): this; + once( + channel: 'media-info-reply', + listener: (event: Event, error?: Error, info: string) => void, + ): this; + once( + channel: 'snapshot-reply', + listener: (event: Event, error?: Error, path: string) => void, + ): this; + once( + channel: 'subtitle-metadata-reply', + listener: (event: Event, error?: Error, finished: boolean, matadata?: string) => void, + ): this; + once( + channel: 'subtitle-cache-reply', + listener: (event: Event, error?: Error, finished: boolean, payload?: string) => void, + ): this; + once( + channel: 'subtitle-stream-reply', + listener: (event: Event, error?: Error, dialogue: string) => void, + ): this; once(channel: 'subtitle-destroy-reply', listener: (event: Event, error?: Error) => void): this; - once(channel: 'thumbnail-reply', listener: (event: Event, error?: Error, path: string) => void): this; + once( + channel: 'thumbnail-reply', + listener: (event: Event, error?: Error, path: string) => void, + ): this; } interface Event { reply(channel: string, ...args: any[]): void; reply(channel: 'media-info-reply', error?: Error, info: string): void; reply(channel: 'snapshot-reply', error?: Error, path: string): this; - reply(channel: 'subtitle-metadata-reply', error?: Error, finished: boolean, matadata: string): this; + reply( + channel: 'subtitle-metadata-reply', + error?: Error, + finished: boolean, + matadata: string, + ): this; reply(channel: 'subtitle-cache-reply', error?: Error, finished: boolean): this; reply(channel: 'subtitle-stream-reply', error?: Error, dialogue: string): this; reply(channel: 'subtitle-destroy-reply', error?: Error): this; diff --git a/src/shared/Fetcher.ts b/src/shared/Fetcher.ts new file mode 100644 index 0000000000..9bfb03667b --- /dev/null +++ b/src/shared/Fetcher.ts @@ -0,0 +1,90 @@ +const fetch: (input: RequestInfo, init?: RequestInit) => Promise = typeof window !== 'undefined' ? window.fetch : require('node-fetch'); +// @ts-ignore +const Headers = typeof window !== 'undefined' ? window.Headers : require('node-fetch').Headers; +// @ts-ignore +const AbortController = typeof window !== 'undefined' ? window.AbortController : require('abort-controller'); + +type FetcherOptions = { + timeout: number; + headers: Headers; + responseInterceptors: ((response: Response) => Response)[]; +}; + +export default class Fetcher { + private options: FetcherOptions; + + public constructor(options?: Optional) { + this.options = Object.assign( + { + timeout: 10000, + headers: new Headers(), + responseInterceptors: [], + }, + options, + ); + } + + public setHeader(name: string, value: string) { + this.options.headers.set(name, value); + } + + public useResponseInterceptor(interceptor: (response: Response) => Response) { + this.options.responseInterceptors.unshift(interceptor); + } + + public fetch(input: RequestInfo, init?: RequestInit): AbortablePromise { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), this.options.timeout); + init = Object.assign( + { + signal: controller.signal, + }, + init, + ); + if (init.headers) { + const headers = new Headers(this.options.headers); + const incomingHeaders = new Headers(init.headers); + incomingHeaders.forEach((v: string, k: string) => { + headers.set(k, v); + }); + init.headers = headers; + } else { + init.headers = this.options.headers; + } + const res = new Promise((resolve, reject) => { + fetch(input, init) + .then( + (response) => { + try { + const responseInterceptors = [ + ...this.options.responseInterceptors, + (r: Response) => r, + ]; + response = responseInterceptors.reduce((r, interceptor) => interceptor(r), response); + resolve(response); + } catch (ex) { + reject(ex); + } + }, + err => reject(err), + ) + .finally(() => clearTimeout(timeout)); + }) as AbortablePromise; + res.abort = () => { + controller.abort(); + }; + return res; + } + + public get(url: string) { + return this.fetch(url, { method: 'GET' }); + } + + public post(url: string, body: Json) { + return this.fetch(url, { + method: 'POST', + body: JSON.stringify(body), + headers: new Headers({ 'Content-Type': 'application/json' }), + }); + } +} diff --git a/src/shared/sentry.js b/src/shared/sentry.js index ad548e1bcb..a1ba25280b 100644 --- a/src/shared/sentry.js +++ b/src/shared/sentry.js @@ -1,5 +1,4 @@ // Be sure to call Sentry function as early as possible in the renderer process -import Vue from 'vue'; import { crashReporter } from 'electron'; import * as Sentry from '@sentry/electron'; import * as Integrations from '@sentry/integrations'; @@ -10,12 +9,12 @@ if (process.env.NODE_ENV !== 'development') { release: process.env.SENTRY_RELEASE, dsn: 'https://6a94feb674b54686a6d88d7278727b7c@sentry.io/1449341', enableNative: false, - integrations: [ + integrations: process.type === 'renderer' ? [ new Integrations.Vue({ - Vue, + Vue: require('vue'), // eslint-disable attachProps: true, }), - ], + ] : [], }); } diff --git a/src/shared/utils.js b/src/shared/utils.js index e6158828e7..bca290a098 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -1,16 +1,20 @@ -import axios from 'axios'; import electron from 'electron'; import { join } from 'path'; -import { - checkPathExist, read, write, -} from '../renderer/libs/file'; +import osLocale from 'os-locale'; +import uuidv4 from 'uuid/v4'; +import storage from 'electron-json-storage'; +import { checkPathExist, read, write } from '../renderer/libs/file'; import { ELECTRON_CACHE_DIRNAME, TOKEN_FILE_NAME } from '../renderer/constants'; import electronBuilderConfig from '../../electron-builder.json'; +import Fetcher from './Fetcher'; const app = electron.app || electron.remote.app; +const fetcher = new Fetcher(); const tokenPath = join(app.getPath(ELECTRON_CACHE_DIRNAME), TOKEN_FILE_NAME); -const subtitleExtensions = Object.freeze(['srt', 'ass', 'vtt', 'ssa'].map(ext => ext.toLowerCase())); +const subtitleExtensions = Object.freeze( + ['srt', 'ass', 'vtt', 'ssa'].map(ext => ext.toLowerCase()), +); export function getValidSubtitleExtensions() { return subtitleExtensions; } @@ -25,13 +29,14 @@ export function getValidSubtitleRegex() { let validVideoExtensions; export function getValidVideoExtensions() { if (validVideoExtensions) return validVideoExtensions; - validVideoExtensions = electronBuilderConfig[process.platform === 'darwin' ? 'mac' : 'win'] - .fileAssociations.reduce((exts, fa) => { - if (!fa || !fa.ext || !fa.ext.length) return exts; - return exts.concat( - fa.ext.map(x => x.toLowerCase()).filter(x => !getValidSubtitleExtensions().includes(x)), - ); - }, []); + validVideoExtensions = electronBuilderConfig[ + process.platform === 'darwin' ? 'mac' : 'win' + ].fileAssociations.reduce((exts, fa) => { + if (!fa || !fa.ext || !fa.ext.length) return exts; + return exts.concat( + fa.ext.map(x => x.toLowerCase()).filter(x => !getValidSubtitleExtensions().includes(x)), + ); + }, []); validVideoExtensions = Object.freeze(validVideoExtensions); return validVideoExtensions; } @@ -46,32 +51,72 @@ export function getValidVideoRegex() { let allValidExtensions; export function getAllValidExtensions() { if (allValidExtensions) return allValidExtensions; - allValidExtensions = electronBuilderConfig[process.platform === 'darwin' ? 'mac' : 'win'] - .fileAssociations.reduce((exts, fa) => { - if (!fa || !fa.ext || !fa.ext.length) return exts; - return exts.concat( - fa.ext.map(x => x.toLowerCase()), - ); - }, []); + allValidExtensions = electronBuilderConfig[ + process.platform === 'darwin' ? 'mac' : 'win' + ].fileAssociations.reduce((exts, fa) => { + if (!fa || !fa.ext || !fa.ext.length) return exts; + return exts.concat(fa.ext.map(x => x.toLowerCase())); + }, []); allValidExtensions = Object.freeze(allValidExtensions); return allValidExtensions; } -export function getIP() { - return new Promise((resolve, reject) => { - axios.get('https://ip.xindong.com/myip', { responseType: 'text' }) - .then((response) => { - resolve(response.data); - }, (error) => { - reject(error); - }); - }); +export function getSystemLocale() { + const { app } = electron.remote; + let locale = process.platform === 'win32' ? app.getLocale() : osLocale.sync(); + locale = locale.replace('_', '-'); + if (locale === 'zh-TW' || locale === 'zh-HK' || locale === 'zh-Hant') { + return 'zh-Hant'; + } + if (locale.startsWith('zh')) { + return 'zh-Hans'; + } + return 'en'; } -export async function getUserInfo(token) { - axios.defaults.headers.common['X-Application-Token'] = token; - return axios.post(''); +if (process.type === 'browser') { + const crossThreadCache = {}; + app.getCrossThreadCache = key => crossThreadCache[key]; + app.setCrossThreadCache = (key, val) => { + crossThreadCache[key] = val; + }; } +function crossThreadCache(key, fn) { + const func = async () => { + if (typeof app.getCrossThreadCache !== 'function') return fn(); + let val = app.getCrossThreadCache(key); + if (val) return val; + val = await fn(); + app.setCrossThreadCache(key, val); + return val; + }; + func.noCache = fn; + return func; +} + +export const getIP = crossThreadCache('ip', async () => { + const res = await fetcher.get('https://ip.xindong.com/myip'); + const ip = await res.text(); + return ip; +}); + +export const getClientUUID = crossThreadCache( + 'clientUUID', + () => new Promise((resolve) => { + if (process.env.NODE_ENV === 'testing') { + resolve('00000000-0000-0000-0000-000000000000'); + return; + } + storage.get('user-uuid', (err, userUUID) => { + if (err || Object.keys(userUUID).length === 0) { + if (err) console.error(err); + userUUID = uuidv4(); + storage.set('user-uuid', userUUID); + } + resolve(userUUID); + }); + }), +); export async function getToken() { try { @@ -81,7 +126,8 @@ export async function getToken() { if (token) { let displayName = ''; try { - displayName = JSON.parse(new Buffer(token.split('.')[1], 'base64').toString()).display_name; // eslint-disable-line + displayName = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()) + .display_name; // eslint-disable-line } catch (error) { // tmpty }