Downloading without visual feedback

This commit is contained in:
Izuco 2022-02-05 13:18:19 +01:00
parent 66c56f3522
commit 7dc72a1507
No known key found for this signature in database
GPG key ID: E9CBE9E4EF3A1BFA
20 changed files with 238 additions and 83 deletions

View file

@ -46,7 +46,8 @@ export type CrunchyEpMeta = {
episodeTitle: string,
seasonID: string,
season: number,
showID: string
showID: string,
e: string
}
export type DownloadedMedia = {

View file

@ -1,11 +1,5 @@
export type ProgressData = {
total: number,
cur: number,
percent: number|string,
time: number,
downloadSpeed: number
};
declare module 'hls-download' {
import type { ProgressData } from './messageHandler';
export type HLSCallback = (data: ProgressData) => unknown;
export default class hlsDownload {
constructor(options: {

View file

@ -9,7 +9,8 @@ export interface MessageHandler {
availableDubCodes: () => Promise<string[]>,
handleDefault: (name: string) => Promise<any>,
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
listEpisodes: (id: string) => Promise<EpisodeListResponse>
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
downloadItem: (data) => void
}
export type QueueItem = {
@ -61,7 +62,7 @@ export type FuniEpisodeData = {
episode: string,
episodeID: string,
seasonTitle: string,
seasonNumber: string
seasonNumber: string,
};
export type AuthData = { username: string, password: string };
@ -72,6 +73,7 @@ export type FuniStreamData = { q: number, callback?: HLSCallback, x: number, fil
timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean,
forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean }
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[] }
export type DownloadData = { id: string, e: string, dubLang: string[], fileName: string, q: number }
export type AuthResponse = ResponseBase<undefined>;
export type FuniSearchReponse = ResponseBase<FunimationSearch>;
@ -79,6 +81,7 @@ export type FuniShowResponse = ResponseBase<FuniEpisodeData[]>;
export type FuniGetEpisodeResponse = ResponseBase<undefined>;
export type CheckTokenResponse = ResponseBase<undefined>;
export type ResponseBase<T> = ({
isOk: true,
value: T
@ -87,4 +90,12 @@ export type ResponseBase<T> = ({
reason: Error
});
export type ProgressData = {
total: number,
cur: number,
percent: number|string,
time: number,
downloadSpeed: number
};
export type PossibleMessanges = keyof ServiceHandler;

View file

@ -1,5 +1,6 @@
import { ProgressData } from "hls-download";
export type RandomEvents = {
progress: ProgressData
progress: ProgressData,
finish: undefined
}

3
TODO.md Normal file
View file

@ -0,0 +1,3 @@
- [ ] Hls-Download force yes or no on rewrite promt
- [ ] Pick up if a download is currently in progress
- [ ] Send more information with the progress event like the title and image to display more information

View file

@ -253,7 +253,7 @@ export default class Crunchy implements ServiceClass {
}
else{
if(Date.now() > new Date(this.token.expires).getTime()){
console.log('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.');
//console.log('[WARN] The token has expired compleatly. I will try to refresh the token anyway, but you might have to reauth.');
}
const authData = new URLSearchParams({
'refresh_token': this.token.refresh_token,
@ -387,7 +387,6 @@ export default class Crunchy implements ServiceClass {
const toSend = searchResults.items.filter(a => a.type === 'series' || a.type === 'movie_listing');
return { isOk: true, value: toSend.map(a => {
return a.items.map((a): SearchResponseItem => {
fs.writeFileSync('../test.json', JSON.stringify(a.images.poster_tall, null, 2));
const images = (a.images.poster_tall ?? [[ { source: '/notFound.png' } ]])[0];
return {
id: a.id,
@ -723,24 +722,6 @@ export default class Crunchy implements ServiceClass {
if(item.season_title == '' && item.series_title == ''){
item.season_title = 'NO_TITLE';
}
// set data
const epMeta: CrunchyEpMeta = {
data: [
{
mediaId: item.id
}
],
seasonTitle: item.season_title,
episodeNumber: item.episode,
episodeTitle: item.title,
seasonID: item.season_id,
season: item.season_number,
showID: id
};
if(item.playback){
epMeta.data[0].playback = item.playback;
}
// find episode numbers
const epNum = item.episode;
let isSpecial = false;
item.isSelected = false;
@ -756,6 +737,25 @@ export default class Crunchy implements ServiceClass {
? 'S' + epNumList.sp.toString().padStart(epNumLen, '0')
: '' + parseInt(epNum, 10).toString().padStart(epNumLen, '0')
);
// set data
const epMeta: CrunchyEpMeta = {
data: [
{
mediaId: item.id
}
],
seasonTitle: item.season_title,
episodeNumber: item.episode,
episodeTitle: item.title,
seasonID: item.season_id,
season: item.season_number,
showID: id,
e: selEpId
};
if(item.playback){
epMeta.data[0].playback = item.playback;
}
// find episode numbers
if((but && item.playback && !doEpsFilter.isSelected([selEpId, item.id])) || (all && item.playback) || (!but && doEpsFilter.isSelected([selEpId, item.id]) && !item.isSelected && item.playback)){
selectedMedia.push(epMeta);
item.isSelected = true;
@ -1239,7 +1239,7 @@ export default class Crunchy implements ServiceClass {
onlyVid: [],
skipSubMux: options.skipSubMux,
onlyAudio: [],
output: `${options}.${options.mp4 ? 'mp4' : 'mkv'}`,
output: `${options.output}.${options.mp4 ? 'mp4' : 'mkv'}`,
subtitles: data.filter(a => a.type === 'Subtitle').map((a) : SubtitleInput => {
if (a.type === 'Video')
throw new Error('Never');
@ -1388,6 +1388,7 @@ export default class Crunchy implements ServiceClass {
if(item.season_title == '' && item.series_title == ''){
item.season_title = 'NO_TITLE';
}
const epNum = key.startsWith('E') ? key.slice(1) : key;
// set data
const epMeta: CrunchyEpMeta = {
data: [
@ -1400,12 +1401,12 @@ export default class Crunchy implements ServiceClass {
episodeTitle: item.title,
seasonID: item.season_id,
season: item.season_number,
showID: item.series_id
showID: item.series_id,
e: epNum
};
if(item.playback){
epMeta.data[0].playback = item.playback;
}
const epNum = key.startsWith('E') ? key.slice(1) : key;
// find episode numbers
if(item.playback && ((but && !doEpsFilter.isSelected([epNum, item.id])) || (all || (doEpsFilter.isSelected([epNum, item.id])) && !but))) {
if (Object.prototype.hasOwnProperty.call(ret, key)) {

View file

@ -17,7 +17,6 @@ let mainWindow: BrowserWindow|undefined = undefined;
export { mainWindow };
const createWindow = async () => {
registerMessageHandler();
// Create the browser window.
mainWindow = new BrowserWindow({
height: 600,
@ -29,6 +28,7 @@ const createWindow = async () => {
},
});
registerMessageHandler(mainWindow);
if (!process.env.USE_BROWSER) {
const app = express();

View file

@ -1,16 +1,15 @@
import { ipcMain } from 'electron';
import { BrowserWindow, ipcMain } from 'electron';
import { MessageHandler } from '../../../@types/messageHandler';
import Crunchy from './serviceHandler/crunchyroll';
import Funimation from './serviceHandler/funimation';
import { mainWindow } from './';
export default () => {
export default (window: BrowserWindow) => {
let handler: MessageHandler|undefined;
ipcMain.handle('setup', (_, data) => {
if (data === 'funi') {
handler = new Funimation();
handler = new Funimation(window);
} else if (data === 'crunchy') {
handler = new Crunchy();
handler = new Crunchy(window);
}
});
@ -22,8 +21,10 @@ export default () => {
ipcMain.handle('availableDubCodes', async () => handler?.availableDubCodes());
ipcMain.handle('resolveItems', async (_, data) => handler?.resolveItems(data));
ipcMain.handle('listEpisodes', async (_, data) => handler?.listEpisodes(data));
ipcMain.handle('downloadItem', async (_, data) => handler?.downloadItem(data));
/*
setInterval(() => {
mainWindow?.webContents.send('progress', { hello: 'World' });
}, 1000);
*/
};

View file

@ -1,2 +1,35 @@
import { BrowserWindow, dialog } from "electron";
import { ProgressData } from "../../../../@types/messageHandler";
export default class Base {
constructor(private window: BrowserWindow) {}
private downloading = false;
setDownloading(downloading: boolean) {
this.downloading = downloading;
}
getDownloading() {
return this.downloading;
}
alertError(error: Error) {
dialog.showMessageBoxSync(this.window, {
message: `${error.name ?? 'An error occured'}\n${error.message}`,
detail: error.stack,
title: `Error`,
type: 'error'
})
}
handleProgress(data: ProgressData) {
this.window.webContents.send('progress', data);
}
getWindow() {
return this.window;
}
}

View file

@ -1,14 +1,17 @@
import { AuthData, CheckTokenResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import { BrowserWindow } from 'electron';
import { CrunchyDownloadOptions } from '../../../../@types/crunchyTypes';
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import Crunchy from '../../../../crunchy';
import Funimation from '../../../../funi';
import { getDefault } from '../../../../modules/module.args';
import { ArgvType } from '../../../../modules/module.app-args';
import { buildDefault, getDefault } from '../../../../modules/module.args';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
import Base from './base';
class CrunchyHandler extends Base implements MessageHandler {
private crunchy: Crunchy;
constructor() {
super();
constructor(window: BrowserWindow) {
super(window);
this.crunchy = new Crunchy();
}
@ -30,23 +33,24 @@ class CrunchyHandler extends Base implements MessageHandler {
return res;
return { isOk: true, value: res.value.map(a => {
return {
...data,
ids: a.data.map(a => a.mediaId),
title: a.episodeTitle,
parent: {
title: a.seasonTitle,
season: a.season.toString()
},
...data
e: a.e
};
}) };
}
public async search(data: SearchData): Promise<SearchResponse> {
this.crunchy.refreshToken();
const funiSearch = await this.crunchy.doSearch(data);
if (!funiSearch.isOk)
return funiSearch;
return { isOk: true, value: funiSearch.value };
const crunchySearch = await this.crunchy.doSearch(data);
if (!crunchySearch.isOk)
return crunchySearch;
return { isOk: true, value: crunchySearch.value };
}
public async checkToken(): Promise<CheckTokenResponse> {
@ -60,6 +64,29 @@ class CrunchyHandler extends Base implements MessageHandler {
public auth(data: AuthData) {
return this.crunchy.doAuth(data);
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
const _default = buildDefault() as ArgvType;
await this.crunchy.refreshToken();
const res = await this.crunchy.downloadFromSeriesID(data.id, {
dubLang: data.dubLang,
e: data.e
});
if (res.isOk) {
for (const select of res.value) {
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callback: this.handleProgress.bind(this), q: data.q, fileName: data.fileName }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
}
}
} else {
this.alertError(res.reason);
}
this.getWindow().webContents.send('finish');
this.setDownloading(false);
}
}
export default CrunchyHandler;

View file

@ -1,13 +1,15 @@
import { AuthData, CheckTokenResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import { BrowserWindow } from 'electron';
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import Funimation from '../../../../funi';
import { getDefault } from '../../../../modules/module.args';
import { ArgvType } from '../../../../modules/module.app-args';
import { buildDefault, getDefault } from '../../../../modules/module.args';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
import Base from './base';
class FunimationHandler extends Base implements MessageHandler {
private funi: Funimation;
constructor() {
super();
constructor(window: BrowserWindow) {
super(window);
this.funi = new Funimation();
}
@ -43,13 +45,14 @@ class FunimationHandler extends Base implements MessageHandler {
return res;
return { isOk: true, value: res.value.map(a => {
return {
...data,
ids: [a.episodeID],
title: a.title,
parent: {
title: a.seasonTitle,
season: a.seasonNumber
},
...data
e: a.episodeID
};
}) };
}
@ -75,6 +78,20 @@ class FunimationHandler extends Base implements MessageHandler {
public auth(data: AuthData) {
return this.funi.auth(data);
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
const res = await this.funi.getShow(false, { all: false, but: false, id: parseInt(data.id), e: data.e });
const _default = buildDefault() as ArgvType;
if (!res.isOk)
return this.alertError(res.reason);
for (const ep of res.value) {
await this.funi.getEpisode(false, { dubLang: data.dubLang, fnSlug: ep, s: data.id, subs: { dlsubs: ['all'], sub: false }, callback: this.handleProgress.bind(this) }, { ..._default, ass: true, callback: this.handleProgress.bind(this), fileName: data.fileName, q: data.q })
}
this.getWindow().webContents.send('finish');
this.setDownloading(false);
};
}
export default FunimationHandler;

View file

@ -0,0 +1,40 @@
import React from "react";
import { ProgressData } from "../../../../../../@types/messageHandler";
import useStore from "../../../hooks/useStore";
import { messageChannelContext } from "../../../provider/MessageChannel";
const useDownloadManager = () => {
const [ { currentDownload }, dispatch ] = useStore();
const messageHandler = React.useContext(messageChannelContext);
const [progressData, setProgressData] = React.useState<ProgressData|undefined>();
React.useEffect(() => {
const handler = (data: ProgressData) => {
console.log(data);
setProgressData(data);
}
messageHandler?.randomEvents.on('progress', handler);
const finishHandler = () => {
console.log('DONE');
}
messageHandler?.randomEvents.on('finish', finishHandler);
return () => {
messageHandler?.randomEvents.removeListener('progress', handler);
messageHandler?.randomEvents.removeListener('finish', finishHandler)
};
}, [messageHandler]);
React.useEffect(() => {
if (!currentDownload)
return;
messageHandler?.downloadItem(currentDownload);
}, [currentDownload]);
return progressData;
}
export default useDownloadManager;

View file

@ -3,7 +3,6 @@ import { Box, Button, Checkbox, Chip, FormControl, FormControlLabel, IconButton,
import useStore from "../../../hooks/useStore";
import MultiSelect from "../../MultiSelect";
import { messageChannelContext } from "../../../provider/MessageChannel";
import { Check, Close } from "@mui/icons-material";
import LoadingButton from '@mui/lab/LoadingButton';
import { useSnackbar } from "notistack";

View file

@ -1,18 +1,10 @@
import React from "react";
import { messageChannelContext } from "../../../provider/MessageChannel";
import { ProgressData } from '../../../../../../@types/hls-download';
import useDownloadManager from "../DownloadManager/DownloadManager";
const Progress: React.FC = () => {
const messageHandler = React.useContext(messageChannelContext);
React.useEffect(() => {
const handler = (data: ProgressData) => {
}
messageHandler?.randomEvents.on('progress', handler);
return () => messageHandler?.randomEvents.removeListener('progress', handler);
}, [messageHandler]);
useDownloadManager();
return <></>
}

View file

@ -23,7 +23,7 @@ const MenuProps = {
function getStyles(name: string, personName: readonly string[], theme: Theme) {
return {
fontWeight:
personName.indexOf(name) === -1
(personName ?? []).indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium
};
@ -39,7 +39,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
labelId="multi-select-label"
id="multi-select"
multiple
value={props.selected}
value={(props.selected ?? [])}
onChange={e => {
const val = typeof e.target.value === "string" ? e.target.value.split(",") : e.target.value;
if (props.allOption && val.includes('all')) {

View file

@ -13,7 +13,8 @@ export class RandomEventHandler {
private handler: {
[eventName in keyof RandomEvents]: Handler<RandomEvents[eventName]>[]
} = {
progress: []
progress: [],
finish: []
};
private allHandler: Handler<unknown>[] = [];
@ -52,6 +53,10 @@ const MessageChannelProvider: React.FC = ({ children }) => {
const { ipcRenderer } = (window as any).Electron as { ipcRenderer: IpcRenderer };
const [ randomEventHandler ] = React.useState(new RandomEventHandler());
const buildListener = (event: keyof RandomEvents) => {
return (_: IpcRendererEvent, ...data: any[]) => randomEventHandler.emit(event, data.length === 0 ? undefined : data[0]);
}
React.useEffect(() => {
(async () => {
const currentService = await ipcRenderer.invoke('type');
@ -63,9 +68,14 @@ const MessageChannelProvider: React.FC = ({ children }) => {
}, [store.service])
React.useEffect(() => {
const listener = (_: IpcRendererEvent, ...data: any[]) => randomEventHandler.emit('progress', data.length === 0 ? undefined : data[0]);
ipcRenderer.on('progress', listener);
return () => ipcRenderer.removeListener('progress', listener) as unknown as void;
const progressListener = buildListener('progress');
const finishListener = buildListener('finish');
ipcRenderer.on('progress', progressListener);
ipcRenderer.on('finish', finishListener);
return () => {
ipcRenderer.removeListener('progress', progressListener);
ipcRenderer.removeListener('finish', finishListener);
};
}, [ ipcRenderer ]);
@ -77,7 +87,8 @@ const MessageChannelProvider: React.FC = ({ children }) => {
availableDubCodes: async () => await ipcRenderer.invoke('availableDubCodes'),
resolveItems: async (data) => await ipcRenderer.invoke('resolveItems', data),
listEpisodes: async (data) => await ipcRenderer.invoke('listEpisodes', data),
randomEvents: randomEventHandler
randomEvents: randomEventHandler,
downloadItem: (data) => ipcRenderer.invoke('downloadItem', data)
}
return <messageChannelContext.Provider value={messageHandler}>

View file

@ -16,7 +16,8 @@ export type StoreState = {
queue: QueueItem[],
episodeListing: Episode[];
downloadOptions: DownloadOptions,
service: 'crunchy'|'funi'|undefined
service: 'crunchy'|'funi'|undefined,
currentDownload?: QueueItem
}
export type StoreAction<T extends keyof StoreState> = {
@ -27,7 +28,12 @@ export type StoreAction<T extends keyof StoreState> = {
const Reducer = <T extends keyof StoreState,>(state: StoreState, action: StoreAction<T>): StoreState => {
switch(action.type) {
case "queue":
return { ...state, queue: state.queue.concat(action.payload as QueueItem[]) };
let queue = state.queue.concat(action.payload as QueueItem[]);
if (!state.currentDownload && queue.length > 0) {
state.currentDownload = queue[0];
queue = queue.slice(1);
}
return { ...state, queue };
default:
return { ...state, [action.type]: action.payload }
}

View file

@ -588,9 +588,27 @@ const getDefault = <T extends boolean|string|number|unknown[]>(name: string, cfg
}
};
const buildDefault = () => {
const data: Record<string, unknown> = {}
const defaultArgs = args.filter(a => a.default);
defaultArgs.forEach(item => {
if (typeof item.default === 'object') {
if (Array.isArray(item.default)) {
data[item.name] = item.default;
} else {
data[item.default.name ?? item.name] = item.default.default;
}
} else {
data[item.name] = item.default;
}
})
return data;
}
export {
TAppArg,
getDefault,
buildDefault,
args,
groups,
availableFilenameVars

14
package-lock.json generated
View file

@ -17,7 +17,7 @@
"form-data": "^4.0.0",
"fs-extra": "^10.0.0",
"got": "^11.8.3",
"hls-download": "^2.6.7",
"hls-download": "^2.6.8",
"iso-639": "^0.2.2",
"lookpath": "^1.1.0",
"m3u8-parsed": "^1.3.0",
@ -6640,9 +6640,9 @@
}
},
"node_modules/hls-download": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/hls-download/-/hls-download-2.6.7.tgz",
"integrity": "sha512-74etN9G6LjDgCCuv0cvNPIUXOFNHWxXA6tyu10/cZSn6wYhF7L8c4QnE/MKpWr557HIDaQJJjWsg8NBAre24LQ==",
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/hls-download/-/hls-download-2.6.8.tgz",
"integrity": "sha512-2ji6wzZpEGOL5C+VRhKmSbUvXER1MGnEgpknoetPgpvmAiV6OwbsIVhYo/t5tHTnaeZ0H2dnE5w6nF6Nn0Ij7w==",
"dependencies": {
"got": "^11.8.3",
"proxy-agent": "^5.0.0",
@ -18364,9 +18364,9 @@
"dev": true
},
"hls-download": {
"version": "2.6.7",
"resolved": "https://registry.npmjs.org/hls-download/-/hls-download-2.6.7.tgz",
"integrity": "sha512-74etN9G6LjDgCCuv0cvNPIUXOFNHWxXA6tyu10/cZSn6wYhF7L8c4QnE/MKpWr557HIDaQJJjWsg8NBAre24LQ==",
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/hls-download/-/hls-download-2.6.8.tgz",
"integrity": "sha512-2ji6wzZpEGOL5C+VRhKmSbUvXER1MGnEgpknoetPgpvmAiV6OwbsIVhYo/t5tHTnaeZ0H2dnE5w6nF6Nn0Ij7w==",
"requires": {
"got": "^11.8.3",
"proxy-agent": "^5.0.0",

View file

@ -34,7 +34,7 @@
"form-data": "^4.0.0",
"fs-extra": "^10.0.0",
"got": "^11.8.3",
"hls-download": "^2.6.7",
"hls-download": "^2.6.8",
"iso-639": "^0.2.2",
"lookpath": "^1.1.0",
"m3u8-parsed": "^1.3.0",