Merge pull request #493 from anidl/4.1.0
4.1.0 -- Numerous fixes and improvements
This commit is contained in:
commit
bfe4ae6164
15 changed files with 1769 additions and 1654 deletions
2
.github/workflows/auto-documentation.yml
vendored
2
.github/workflows/auto-documentation.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
- name: Use Node.js 16
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
- run: pnpm i
|
||||
- run: pnpm run docs
|
||||
- uses: stefanzweifel/git-auto-commit-action@v4
|
||||
|
|
|
|||
2
.github/workflows/release-matrix.yml
vendored
2
.github/workflows/release-matrix.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
check-latest: true
|
||||
- name: Install Node modules
|
||||
run: |
|
||||
|
|
|
|||
|
|
@ -1644,6 +1644,9 @@ export default class Crunchy implements ServiceClass {
|
|||
ret[season_number][lang.code] = item;
|
||||
} else if (item.is_dubbed && lang.code === 'eng' && !langsData.languages.some(a => item.title.includes(`(${a.name})`) || item.title.includes(`(${a.name} Dub)`))) { // Dubbed with no more infos will be treated as eng dubs
|
||||
ret[season_number][lang.code] = item;
|
||||
//TODO: look into if below is stable
|
||||
} else if (item.audio_locale == lang.cr_locale) {
|
||||
ret[season_number][lang.code] = item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# multi-downloader-nx (4.0.3v)
|
||||
# multi-downloader-nx (4.1.0v)
|
||||
|
||||
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
||||
|
||||
|
|
@ -136,6 +136,12 @@ This will speed up the download speed, if multiple languages are selected.
|
|||
|
||||
If selected, it will remove the bumpers such as the hidive intro from the final file.
|
||||
Currently disabling this sometimes results in bugs such as video/audio desync
|
||||
#### `--originalFontSize`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | ---|
|
||||
| Hidive | `--originalFontSize ` | `boolean` | `No`| `NaN` | `true`| `originalFontSize: ` |
|
||||
|
||||
If selected, it will prefer to keep the original Font Size defined by the service.
|
||||
#### `-x`
|
||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -2,12 +2,15 @@ import { Box, List, ListItem, Typography, Divider, Dialog, Select, MenuItem, For
|
|||
import { CheckBox, CheckBoxOutlineBlank } from '@mui/icons-material';
|
||||
import React from 'react';
|
||||
import useStore from '../../../../hooks/useStore';
|
||||
import ContextMenu from '../../../reusable/ContextMenu';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
|
||||
const EpisodeListing: React.FC = () => {
|
||||
const [store, dispatch] = useStore();
|
||||
|
||||
const [season, setSeason] = React.useState<'all'|string>('all');
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const seasons = React.useMemo(() => {
|
||||
const s: string[] = [];
|
||||
|
|
@ -72,28 +75,26 @@ const EpisodeListing: React.FC = () => {
|
|||
</ListItem>
|
||||
{store.episodeListing.filter((a) => season === 'all' ? true : a.season === season).map((item, index, { length }) => {
|
||||
const e = isNaN(parseInt(item.e)) ? item.e : parseInt(item.e);
|
||||
const idStr = `S${item.season}E${e}`
|
||||
const isSelected = selected.includes(e.toString());
|
||||
return <Box {...{ mouseData: isSelected }} key={`Episode_List_Item_${index}`} sx={{
|
||||
backdropFilter: isSelected ? 'brightness(1.5)' : '',
|
||||
'&:hover': {
|
||||
backdropFilter: 'brightness(1.5)'
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
let arr: string[] = [];
|
||||
if (isSelected) {
|
||||
arr = [...selected.filter(a => a !== e.toString())];
|
||||
} else {
|
||||
arr = [...selected, e.toString()];
|
||||
}
|
||||
setSelected(arr.filter(a => a.length > 0));
|
||||
}}>
|
||||
<ListItem sx={{ display: 'grid', gridTemplateColumns: '25px 50px 1fr 5fr' }}>
|
||||
const imageRef = React.createRef<HTMLImageElement>();
|
||||
const summaryRef = React.createRef<HTMLParagraphElement>();
|
||||
return <Box {...{ mouseData: isSelected }} key={`Episode_List_Item_${index}`}>
|
||||
<ListItem sx={{backdropFilter: isSelected ? 'brightness(1.5)' : '', '&:hover': {backdropFilter: 'brightness(1.5)'}, display: 'grid', gridTemplateColumns: '25px 50px 1fr 5fr' }}
|
||||
onClick={() => {
|
||||
let arr: string[] = [];
|
||||
if (isSelected) {
|
||||
arr = [...selected.filter(a => a !== e.toString())];
|
||||
} else {
|
||||
arr = [...selected, e.toString()];
|
||||
}
|
||||
setSelected(arr.filter(a => a.length > 0));
|
||||
}}>
|
||||
{ isSelected ? <CheckBox /> : <CheckBoxOutlineBlank /> }
|
||||
<Typography color='text.primary' sx={{ textAlign: 'center' }}>
|
||||
{e}
|
||||
{idStr}
|
||||
</Typography>
|
||||
<img style={{ width: 'inherit', maxHeight: '200px', minWidth: '150px' }} src={item.img} alt="thumbnail" />
|
||||
<img ref={imageRef} style={{ width: 'inherit', maxHeight: '200px', minWidth: '150px' }} src={item.img} alt="thumbnail" />
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', pl: 1 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr min-content' }}>
|
||||
<Typography color='text.primary' variant="h5">
|
||||
|
|
@ -103,7 +104,7 @@ const EpisodeListing: React.FC = () => {
|
|||
{item.time.startsWith('00:') ? item.time.slice(3) : item.time}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography color='text.primary'>
|
||||
<Typography color='text.primary' ref={summaryRef}>
|
||||
{item.description}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'fit-content 1fr' }}>
|
||||
|
|
@ -114,6 +115,29 @@ const EpisodeListing: React.FC = () => {
|
|||
</Box>
|
||||
</Box>
|
||||
</ListItem>
|
||||
<ContextMenu options={[ { text: 'Copy image URL', onClick: async () => {
|
||||
await navigator.clipboard.writeText(item.img);
|
||||
enqueueSnackbar('Copied URL to clipboard', {
|
||||
variant: 'info'
|
||||
});
|
||||
}},
|
||||
{
|
||||
text: 'Open image in new tab',
|
||||
onClick: () => {
|
||||
window.open(item.img);
|
||||
}
|
||||
} ]} popupItem={imageRef} />
|
||||
<ContextMenu options={[
|
||||
{
|
||||
onClick: async () => {
|
||||
await navigator.clipboard.writeText(item.description!);
|
||||
enqueueSnackbar('Copied summary to clipboard', {
|
||||
variant: 'info'
|
||||
})
|
||||
},
|
||||
text: "Copy summary to clipboard"
|
||||
}
|
||||
]} popupItem={summaryRef} />
|
||||
{index < length - 1 && <Divider />}
|
||||
</Box>;
|
||||
})}
|
||||
|
|
@ -160,4 +184,4 @@ const parseSelect = (s: string): string[] => {
|
|||
return [...new Set(ret)];
|
||||
};
|
||||
|
||||
export default EpisodeListing;
|
||||
export default EpisodeListing;
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const SearchBox: React.FC = () => {
|
|||
React.useEffect(() => {
|
||||
if (search.trim().length === 0)
|
||||
return setSearchResult({ isOk: true, value: [] });
|
||||
|
||||
|
||||
const timeOutId = setTimeout(async () => {
|
||||
if (search.trim().length > 3) {
|
||||
const s = await messageHandler?.search({search});
|
||||
|
|
@ -49,12 +49,13 @@ const SearchBox: React.FC = () => {
|
|||
<Box sx={{ m: 2 }}>
|
||||
<TextField ref={anchor} value={search} onClick={() => setFocus(true)} onChange={e => setSearch(e.target.value)} variant='outlined' label='Search' fullWidth />
|
||||
{searchResult !== undefined && searchResult.isOk && searchResult.value.length > 0 && focus &&
|
||||
<Paper sx={{ position: 'fixed', maxHeight: '50%', width: `${anchorBounding?.width}px`,
|
||||
<Paper sx={{ position: 'fixed', maxHeight: '50%', width: `${anchorBounding?.width}px`,
|
||||
left: anchorBounding?.x, top: (anchorBounding?.y ?? 0) + (anchorBounding?.height ?? 0), zIndex: 99, overflowY: 'scroll'}}>
|
||||
<List>
|
||||
{searchResult && searchResult.isOk ?
|
||||
searchResult.value.map((a, ind, arr) => {
|
||||
const imageRef = React.createRef<HTMLImageElement>();
|
||||
const summaryRef = React.createRef<HTMLParagraphElement>();
|
||||
return <Box key={a.id}>
|
||||
<ListItem className='listitem-hover' onClick={() => {
|
||||
selectItem(a.id);
|
||||
|
|
@ -68,7 +69,7 @@ const SearchBox: React.FC = () => {
|
|||
<Typography variant='h6' component='h6' color='text.primary' sx={{ }}>
|
||||
{a.name}
|
||||
</Typography>
|
||||
{a.desc && <Typography variant='caption' component='p' color='text.primary' sx={{ pt: 1, pb: 1 }}>
|
||||
{a.desc && <Typography variant='caption' component='p' color='text.primary' sx={{ pt: 1, pb: 1 }} ref={summaryRef}>
|
||||
{a.desc}
|
||||
</Typography>}
|
||||
{a.lang && <Typography variant='caption' component='p' color='text.primary' sx={{ }}>
|
||||
|
|
@ -90,8 +91,21 @@ const SearchBox: React.FC = () => {
|
|||
text: 'Open image in new tab',
|
||||
onClick: () => {
|
||||
window.open(a.image);
|
||||
}
|
||||
}
|
||||
} ]} popupItem={imageRef} />
|
||||
{a.desc &&
|
||||
<ContextMenu options={[
|
||||
{
|
||||
onClick: async () => {
|
||||
await navigator.clipboard.writeText(a.desc!);
|
||||
enqueueSnackbar('Copied summary to clipboard', {
|
||||
variant: 'info'
|
||||
})
|
||||
},
|
||||
text: "Copy summary to clipboard"
|
||||
}
|
||||
]} popupItem={summaryRef} />
|
||||
}
|
||||
{(ind < arr.length - 1) && <Divider />}
|
||||
</Box>;
|
||||
})
|
||||
|
|
@ -102,4 +116,4 @@ const SearchBox: React.FC = () => {
|
|||
</ClickAwayListener>;
|
||||
};
|
||||
|
||||
export default SearchBox;
|
||||
export default SearchBox;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,24 @@
|
|||
import { Box, Button, Menu, MenuItem } from '@mui/material';
|
||||
import { Box, Button, Menu, MenuItem, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { messageChannelContext } from '../../provider/MessageChannel';
|
||||
import useStore from '../../hooks/useStore';
|
||||
import { StoreState } from '../../provider/Store'
|
||||
|
||||
const MenuBar: React.FC = () => {
|
||||
const [ openMenu, setMenuOpen ] = React.useState<'settings'|'help'|undefined>();
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const [store] = useStore();
|
||||
|
||||
const transformService = (service: StoreState['service']) => {
|
||||
switch(service) {
|
||||
case 'crunchy':
|
||||
return "Crunchyroll"
|
||||
case 'funi':
|
||||
return "Funimation"
|
||||
case "hidive":
|
||||
return "Hidive"
|
||||
}
|
||||
}
|
||||
|
||||
const msg = React.useContext(messageChannelContext);
|
||||
|
||||
|
|
@ -79,6 +93,9 @@ const MenuBar: React.FC = () => {
|
|||
Discord
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<Typography variant="h5" color="text.primary" component="div" align="center" sx={{flexGrow: 1}}>
|
||||
{transformService(store.service)}
|
||||
</Typography>
|
||||
</Box>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ const buttonSx: SxProps = {
|
|||
|
||||
function ContextMenu<T extends HTMLElement, >(props: ContextMenuProps<T>) {
|
||||
const [anchor, setAnchor] = React.useState( { x: 0, y: 0 } );
|
||||
|
||||
|
||||
const [show, setShow] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
|
@ -37,16 +37,16 @@ function ContextMenu<T extends HTMLElement, >(props: ContextMenuProps<T>) {
|
|||
};
|
||||
ref.current.addEventListener('contextmenu', listener);
|
||||
|
||||
return () => {
|
||||
return () => {
|
||||
if (ref.current)
|
||||
ref.current.removeEventListener('contextmenu', listener);
|
||||
};
|
||||
}, [ props.popupItem ]);
|
||||
|
||||
return show ? <Box sx={{ zIndex: 9999, p: 1, background: 'rgba(0, 0, 0, 0.75)', backdropFilter: 'blur(5px)', position: 'fixed', left: anchor.x, top: anchor.y }}>
|
||||
return show ? <Box sx={{ zIndex: 1400, p: 1, background: 'rgba(0, 0, 0, 0.75)', backdropFilter: 'blur(5px)', position: 'fixed', left: anchor.x, top: anchor.y }}>
|
||||
<List sx={{ p: 0, m: 0, display: 'flex', flexDirection: 'column' }}>
|
||||
{props.options.map((item, i) => {
|
||||
return item === 'divider' ? <Divider key={`ContextMenu_Divider_${i}_${item}`}/> :
|
||||
return item === 'divider' ? <Divider key={`ContextMenu_Divider_${i}_${item}`}/> :
|
||||
<Button color='inherit' key={`ContextMenu_Value_${i}_${item}`} onClick={() => {
|
||||
item.onClick();
|
||||
setShow(false);
|
||||
|
|
@ -62,4 +62,4 @@ function ContextMenu<T extends HTMLElement, >(props: ContextMenuProps<T>) {
|
|||
</Box> : <></>;
|
||||
}
|
||||
|
||||
export default ContextMenu;
|
||||
export default ContextMenu;
|
||||
|
|
|
|||
|
|
@ -416,7 +416,7 @@ export default class Hidive implements ServiceClass {
|
|||
console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`);
|
||||
const videoUrls = videoData.Data.VideoUrls;
|
||||
const subsUrls = videoData.Data.CaptionVttUrls;
|
||||
const fontSize = videoData.Data.FontSize ? videoData.Data.FontSize : 34;
|
||||
const fontSize = videoData.Data.FontSize ? videoData.Data.FontSize : options.fontSize;
|
||||
const subsSel = subsList;
|
||||
//Get Selected Video URLs
|
||||
const videoSel = videoList.sort().filter(videoLanguage =>
|
||||
|
|
@ -490,10 +490,11 @@ export default class Hidive implements ServiceClass {
|
|||
let mediaName = '...';
|
||||
let fileName;
|
||||
const files: DownloadedMedia[] = [];
|
||||
const variables: Variable[] = [];
|
||||
let dlFailed = false;
|
||||
//let dlVideoOnce = false; // Variable to save if best selected video quality was downloaded
|
||||
let subsMargin = 0;
|
||||
const variables: Variable[] = [];
|
||||
const chosenFontSize = options.originalFontSize ? fontSize : options.fontSize;
|
||||
for (const videoData of videoUrls) {
|
||||
if(videoData.seriesTitle && videoData.episodeNumber && videoData.episodeTitle){
|
||||
mediaName = `${videoData.seriesTitle} - ${videoData.episodeNumber} - ${videoData.episodeTitle}`;
|
||||
|
|
@ -700,8 +701,7 @@ export default class Hidive implements ServiceClass {
|
|||
const getVttContent = await this.req.getData(await this.genSubsUrl('vtt', subsXUrl));
|
||||
if (getCssContent.ok && getVttContent.ok && getCssContent.res && getVttContent.res) {
|
||||
//vttConvert(getVttContent.res.body, false, subLang.name, fontSize);
|
||||
//TODO: look into potentially having an option for native fontSize
|
||||
const sBody = vtt(undefined, options.fontSize, getVttContent.res.body, getCssContent.res.body, subsMargin, options.fontName);
|
||||
const sBody = vtt(undefined, chosenFontSize, getVttContent.res.body, getCssContent.res.body, subsMargin, options.fontName);
|
||||
sxData.title = `${subLang.language} / ${sxData.title}`;
|
||||
sxData.fonts = fontsData.assFonts(sBody) as Font[];
|
||||
fs.writeFileSync(sxData.path, sBody);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { execSync } from 'child_process';
|
|||
import { console } from './log';
|
||||
|
||||
const buildsDir = './_builds';
|
||||
const nodeVer = 'node16-';
|
||||
const nodeVer = 'node18-';
|
||||
|
||||
type BuildTypes = `${'ubuntu'|'windows'|'macos'|'arm'}64`
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ let argvC: {
|
|||
$0: string;
|
||||
dlVideoOnce: boolean;
|
||||
removeBumpers: boolean;
|
||||
originalFontSize: boolean;
|
||||
};
|
||||
|
||||
export type ArgvType = typeof argvC;
|
||||
|
|
|
|||
|
|
@ -205,6 +205,18 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
|||
default: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'originalFontSize',
|
||||
describe: 'Keep original font size',
|
||||
type: 'boolean',
|
||||
group: 'dl',
|
||||
service: ['hidive'],
|
||||
docDescribe: 'If selected, it will prefer to keep the original Font Size defined by the service.',
|
||||
usage: '',
|
||||
default: {
|
||||
default: true
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'x',
|
||||
group: 'dl',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "multi-downloader-nx",
|
||||
"short_name": "aniDL",
|
||||
"version": "4.0.3",
|
||||
"version": "4.1.0",
|
||||
"description": "Download videos from Funimation, Crunchyroll, or Hidive via cli",
|
||||
"keywords": [
|
||||
"download",
|
||||
|
|
|
|||
12
src/hooks/useStore.tsx
Normal file
12
src/hooks/useStore.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import { StoreAction, StoreContext, StoreState } from '../provider/Store';
|
||||
|
||||
const useStore = () => {
|
||||
const context = React.useContext(StoreContext as unknown as React.Context<[StoreState, React.Dispatch<StoreAction<keyof StoreState>>]>);
|
||||
if (!context) {
|
||||
throw new Error('useStore must be used under Store');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default useStore;
|
||||
Loading…
Reference in a new issue