Show episode listing

This commit is contained in:
Izuco 2022-02-01 20:57:41 +01:00
parent ea9001d0b3
commit 14de1b243a
No known key found for this signature in database
GPG key ID: E9CBE9E4EF3A1BFA
14 changed files with 196 additions and 38 deletions

2
@types/items.d.ts vendored
View file

@ -34,7 +34,7 @@ export interface Item {
mostRecentSvodUs: MostRecent;
item: Item;
mostRecentSvodEngAllTerrStartTimestamp: number;
audio: Audio[];
audio: string[];
mostRecentAvod: MostRecent;
}

View file

@ -8,7 +8,8 @@ export interface MessageHandler {
search: (data: SearchData) => Promise<SearchResponse>,
availableDubCodes: () => Promise<string[]>,
handleDefault: (name: string) => Promise<any>,
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
listEpisodes: (id: string) => Promise<EpisodeListResponse>
}
export type QueueItem = {
@ -42,7 +43,18 @@ export type SearchResponseItem = {
rating: number
};
export type Episode = {
e: string,
lang: string[],
name: string,
season: string,
seasonTitle: string,
episode: string,
id: string
}
export type SearchResponse = ResponseBase<SearchResponseItem[]>
export type EpisodeListResponse = ResponseBase<Episode[]>
export type FuniEpisodeData = {
title: string,

View file

@ -38,7 +38,7 @@ import { PlaybackData } from './@types/playbackData';
import { downloaded } from './modules/module.downloadArchive';
import parseSelect from './modules/module.parseSelect';
import { AvailableFilenameVars, getDefault } from './modules/module.args';
import { AuthData, AuthResponse, ResponseBase, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
import { AuthData, AuthResponse, Episode, ResponseBase, SearchData, SearchResponse, SearchResponseItem } from './@types/messageHandler';
import { ServiceClass } from './@types/serviceClassInterface';
export default class Crunchy implements ServiceClass {
@ -1279,10 +1279,14 @@ export default class Crunchy implements ServiceClass {
merger.cleanUp();
}
public async downloadFromSeriesID(id: string, data: CurnchyMultiDownload) : Promise<ResponseBase<CrunchyEpMeta[]>> {
public async listSeriesID(id: string): Promise<{ list: Episode[], data: Record<string, {
items: Item[];
langs: langsData.LanguageItem[];
}>}> {
await this.refreshToken();
const parsed = await this.parseSeriesById(id);
if (!parsed)
return { isOk: false, reason: new Error('Parse Error') };
throw new Error('Unable to parse')
const result = this.parseSeriesResult(parsed);
const episodes : Record<string, {
items: Item[],
@ -1331,6 +1335,22 @@ export default class Crunchy implements ServiceClass {
}).join(', ')
}]`);
}
return { data: episodes, list: Object.entries(episodes).map(([key, value]) => {
return {
e: key.startsWith('E') ? key.slice(1) : key,
lang: value.langs.map(a => a.code),
name: value.items[0].title,
season: value.items[0].season_number.toString(),
seasonTitle: value.items[0].season_title.replace(/\(\w+ Dub\)/g, '').trimEnd(),
episode: value.items[0].episode_number?.toString() ?? value.items[0].episode ?? '?',
id: value.items[0].season_id
}
})};
}
public async downloadFromSeriesID(id: string, data: CurnchyMultiDownload) : Promise<ResponseBase<CrunchyEpMeta[]>> {
const { data: episodes } = await this.listSeriesID(id);
console.log();
console.log('-'.repeat(30));
console.log();

45
funi.ts
View file

@ -37,7 +37,7 @@ import { FunimationMediaDownload } from './@types/funiTypes';
import * as langsData from './modules/module.langsData';
import { TitleElement } from './@types/episode';
import { AvailableFilenameVars } from './modules/module.args';
import { AuthData, AuthResponse, CheckTokenResponse, FuniGetEpisodeData, FuniGetEpisodeResponse, FuniGetShowData, SearchData, FuniSearchReponse, FuniShowResponse, FuniStreamData, FuniSubsData, FuniEpisodeData } from './@types/messageHandler';
import { AuthData, AuthResponse, CheckTokenResponse, FuniGetEpisodeData, FuniGetEpisodeResponse, FuniGetShowData, SearchData, FuniSearchReponse, FuniShowResponse, FuniStreamData, FuniSubsData, FuniEpisodeData, ResponseBase } from './@types/messageHandler';
import { ServiceClass } from './@types/serviceClassInterface';
// check page
@ -52,6 +52,9 @@ let fnEpNum: string|number = 0,
stDlPath: Subtitle[] = [];
export default class Funi implements ServiceClass {
public static epIdLen = 4;
public static typeIdLen = 0;
public cfg: yamlCfg.ConfigObject;
private token: string | boolean;
@ -170,10 +173,10 @@ export default class Funi implements ServiceClass {
return { isOk: true, value: searchDataJSON };
}
public async getShow(log: boolean, data: FuniGetShowData) : Promise<FuniShowResponse> {
public async listShowItems(id: number) : Promise<ResponseBase<Item[]>> {
const showData = await getData({
baseUrl: api_host,
url: `/source/catalog/title/${data.id}`,
url: `/source/catalog/title/${id}`,
token: this.token,
useToken: true,
debug: this.debug,
@ -182,8 +185,7 @@ export default class Funi implements ServiceClass {
if(!showData.ok || !showData.res){ return { isOk: false, reason: new Error('ShowData is not ok') }; }
const showDataJSON = JSON.parse(showData.res.body);
if(showDataJSON.status){
if (log)
console.log('[ERROR] Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
console.log('[ERROR] Error #%d: %s\n', showDataJSON.status, showDataJSON.data.errors[0].detail);
return { isOk: false, reason: new Error(showDataJSON.data.errors[0].detail) };
}
else if(!showDataJSON.items || showDataJSON.items.length<1){
@ -191,8 +193,7 @@ export default class Funi implements ServiceClass {
return { isOk: false, reason: new Error('Show not found') };
}
const showDataItem = showDataJSON.items[0];
if (log)
console.log('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear);
console.log('[#%s] %s (%s)',showDataItem.id,showDataItem.title,showDataItem.releaseYear);
// show episodes
const qs: {
limit: number,
@ -200,7 +201,7 @@ export default class Funi implements ServiceClass {
sort_direction: string,
title_id: number,
language?: string
} = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: data.id };
} = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: id };
const episodesData = await getData({
baseUrl: api_host,
url: '/funimation/episodes/',
@ -213,7 +214,6 @@ export default class Funi implements ServiceClass {
let epsDataArr: Item[] = JSON.parse(episodesData.res.body).items;
const epNumRegex = /^([A-Z0-9]*[A-Z])?(\d+)$/i;
const epSelEpsTxt = []; let typeIdLen = 0, epIdLen = 4;
const parseEpStr = (epStr: string) => {
const match = epStr.match(epNumRegex);
@ -234,23 +234,18 @@ export default class Funi implements ServiceClass {
e.id = baseId.replace(new RegExp('^' + e.ids.externalShowId), '');
if(e.id.match(epNumRegex)){
const epMatch = parseEpStr(e.id);
epIdLen = epMatch[1].length > epIdLen ? epMatch[1].length : epIdLen;
typeIdLen = epMatch[0].length > typeIdLen ? epMatch[0].length : typeIdLen;
Funi.epIdLen = epMatch[1].length > Funi.epIdLen ? epMatch[1].length : Funi.epIdLen;
Funi.typeIdLen = epMatch[0].length > Funi.typeIdLen ? epMatch[0].length : Funi.typeIdLen;
e.id_split = epMatch;
}
else{
typeIdLen = 3 > typeIdLen? 3 : typeIdLen;
Funi.typeIdLen = 3 > Funi.typeIdLen? 3 : Funi.typeIdLen;
console.log('[ERROR] FAILED TO PARSE: ', e.id);
e.id_split = [ 'ZZZ', 9999 ];
}
return e;
});
const epSelList = parseSelect(data.e as string, data.but);
const fnSlug: FuniEpisodeData[] = []; let is_selected = false;
const eps = epsDataArr;
epsDataArr.sort((a, b) => {
if (a.item.seasonOrder < b.item.seasonOrder && a.id.localeCompare(b.id) < 0) {
return -1;
@ -260,9 +255,21 @@ export default class Funi implements ServiceClass {
}
return 0;
});
return { isOk: true, value: epsDataArr };
}
public async getShow(log: boolean, data: FuniGetShowData) : Promise<FuniShowResponse> {
const showList = await this.listShowItems(data.id);
if (!showList.isOk)
return showList;
const eps = showList.value;
const epSelList = parseSelect(data.e as string, data.but);
const fnSlug: FuniEpisodeData[] = [], epSelEpsTxt = []; let is_selected = false;
for(const e in eps){
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(epIdLen, '0');
eps[e].id_split[1] = parseInt(eps[e].id_split[1].toString()).toString().padStart(Funi.epIdLen, '0');
let epStrId = eps[e].id_split.join('');
// select
is_selected = false;
@ -283,7 +290,7 @@ export default class Funi implements ServiceClass {
const aud_str = eps[e].audio.length > 0 ? `, ${eps[e].audio.join(', ')}` : '';
const rtm_str = eps[e].item.runtime !== '' ? eps[e].item.runtime : '??:??';
// console string
eps[e].id_split[0] = eps[e].id_split[0].toString().padStart(typeIdLen, ' ');
eps[e].id_split[0] = eps[e].id_split[0].toString().padStart(Funi.typeIdLen, ' ');
epStrId = eps[e].id_split.join('');
let conOut = `[${epStrId}] `;
conOut += `${eps[e].item.titleName+tx_snum} - ${tx_type+tx_enum} ${eps[e].item.episodeName} `;

View file

@ -20,4 +20,5 @@ export default () => {
ipcMain.handle('default', async (_, data) => handler?.handleDefault(data));
ipcMain.handle('availableDubCodes', async () => handler?.availableDubCodes());
ipcMain.handle('resolveItems', async (_, data) => handler?.resolveItems(data));
ipcMain.handle('listEpisodes', async (_, data) => handler?.listEpisodes(data));
};

View file

@ -1,4 +1,4 @@
import { AuthData, CheckTokenResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import { AuthData, CheckTokenResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import Crunchy from '../../../../crunchy';
import Funimation from '../../../../funi';
import { getDefault } from '../../../../modules/module.args';
@ -12,6 +12,10 @@ class CrunchyHandler extends Base implements MessageHandler {
this.crunchy = new Crunchy();
}
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
}
public async handleDefault(name: string) {
return getDefault(name, this.crunchy.cfg.cli);
}

View file

@ -1,4 +1,4 @@
import { AuthData, CheckTokenResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import { AuthData, CheckTokenResponse, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import Funimation from '../../../../funi';
import { getDefault } from '../../../../modules/module.args';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
@ -10,6 +10,24 @@ class FunimationHandler extends Base implements MessageHandler {
super();
this.funi = new Funimation();
}
public async listEpisodes (id: string) : Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.funi.listShowItems(parse);
if (!request.isOk)
return request;
return { isOk: true, value: request.value.map(item => ({
e: item.id_split.join(''),
lang: item.audio ?? [],
name: item.episodeSlug,
season: item.seasonNum,
seasonTitle: item.seasonTitle,
episode: item.episodeNum,
id: item.id
})) }
}
public async handleDefault(name: string) {
return getDefault(name, this.funi.cfg.cli);

View file

@ -1,7 +0,0 @@
import React from "react";
const Bottom: React.FC = () => {
return <></>
}
export default Bottom;

View file

@ -0,0 +1,11 @@
import { Box } from "@mui/material";
import React from "react";
import EpisodeListing from "./Listing/EpisodeListing";
const Bottom: React.FC = () => {
return <Box sx={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
<EpisodeListing />
</Box>
}
export default Bottom;

View file

@ -0,0 +1,57 @@
import { Accordion, AccordionSummary, AccordionDetails, Box, List, ListItem, Typography } from "@mui/material";
import { ExpandMore } from '@mui/icons-material'
import React from "react";
import useStore from "../../../../hooks/useStore";
import { Episode } from "../../../../../../../@types/messageHandler";
const EpisodeListing: React.FC = () => {
const [store] = useStore();
const [expended, setExpended] = React.useState('');
const [data, setData] = React.useState<{
[seasonHeader: string]: Episode[]
}>({});
React.useEffect(() => {
const map: {
[seasonHeader: string]: Episode[]
} = {};
store.episodeListing.forEach(item => {
const title = `S${item.season} - ${item.seasonTitle}`;
if (Object.prototype.hasOwnProperty.call(map, title)) {
map[title].push(item);
} else {
map[title] = [ item ];
}
})
setData(map);
}, [store]);
return <Box>
{Object.entries(data).map(([key, items], index) => {
return <Accordion key={`Season_${index}`} expanded={expended === key} onChange={() => setExpended(key === expended ? '' : key)}>
<AccordionSummary
expandIcon={<ExpandMore />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography sx={{ width: '80%', flexShrink: 0 }}>
{key}
</Typography>
</AccordionSummary>
<AccordionDetails>
{items.map((item, index) => {
return <Typography key={`Season_Item_${index}`} sx={{ paddingBottom: 1 }}>
{`[${item.e}] [S${item.season}E${item.episode}] ${item.name} ( ${item.lang.join(', ')} ) `}
</Typography>
})}
</AccordionDetails>
</Accordion>;
})}
</Box>
}
export default EpisodeListing;

View file

@ -5,12 +5,14 @@ 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";
const DownloadSelector: React.FC = () => {
const messageHandler = React.useContext(messageChannelContext);
const [store, dispatch] = useStore();
const [availableDubs, setAvailableDubs] = React.useState<string[]>([]);
const [ loading, setLoading ] = React.useState(false);
const { enqueueSnackbar } = useSnackbar();
React.useEffect(() => {
(async () => {
@ -31,7 +33,11 @@ const DownloadSelector: React.FC = () => {
setLoading(true);
const res = await messageHandler?.resolveItems(store.downloadOptions);
if (!res || !res.isOk) {
window.alert(res?.reason.message ?? 'Unable to resolve data');
console.error(res);
setLoading(false);
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
variant: 'error'
});
} else {
dispatch({
type: 'queue',
@ -41,6 +47,29 @@ const DownloadSelector: React.FC = () => {
setLoading(false);
}
const listEpisodes = async () => {
if (!store.downloadOptions.id) {
return enqueueSnackbar('Please enter a ID', {
variant: 'error'
});
}
setLoading(true);
const res = await messageHandler?.listEpisodes(store.downloadOptions.id);
if (!res || !res.isOk) {
console.log(res);
setLoading(false);
return enqueueSnackbar('The request failed. Please check if the ID is correct.', {
variant: 'error'
});
} else {
dispatch({
type: 'episodeListing',
payload: res.value
});
}
setLoading(false);
}
console.log(store.queue);
return <Box sx={{ display: 'flex', flexDirection: 'column' }}>
@ -87,7 +116,7 @@ const DownloadSelector: React.FC = () => {
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, but: !store.downloadOptions.but } })} variant={store.downloadOptions.but ? 'contained' : 'outlined'}>Download all but</Button>
</Box>
<Box sx={{ gap: 2, flex: 0, m: 1, mb: 3, display: 'flex', justifyContent: 'center' }}>
<LoadingButton loading={loading} onClick={addToQueue} variant='contained'>Search for episodes</LoadingButton>
<LoadingButton loading={loading} onClick={listEpisodes} variant='contained'>Search for episodes</LoadingButton>
<LoadingButton loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
</Box>
</Box>

View file

@ -1,5 +1,6 @@
import { Box, Divider } from "@mui/material";
import React from "react";
import Bottom from "./Bottom/Bottom";
import DownloadSelector from "./DownloadSelector/DownloadSelector";
import './MainFrame.css';
import SearchBox from "./SearchBox/SearchBox";
@ -9,6 +10,8 @@ const MainFrame: React.FC = () => {
<SearchBox />
<Divider variant='middle' className="divider-width" light sx={{ color: 'text.primary', fontSize: '1.2rem' }}>Options</Divider>
<DownloadSelector />
<Divider variant='middle' className="divider-width" light />
<Bottom />
</Box>
}

View file

@ -28,7 +28,8 @@ const MessageChannelProvider: React.FC = ({ children }) => {
search: async (data) => await ipcRenderer.invoke('search', data),
handleDefault: async (data) => await ipcRenderer.invoke('default', data),
availableDubCodes: async () => await ipcRenderer.invoke('availableDubCodes'),
resolveItems: async (data) => await ipcRenderer.invoke('resolveItems', data)
resolveItems: async (data) => await ipcRenderer.invoke('resolveItems', data),
listEpisodes: async (data) => await ipcRenderer.invoke('listEpisodes', data)
}
return <messageChannelContext.Provider value={messageHandler}>

View file

@ -1,5 +1,5 @@
import React from 'react';
import { QueueItem } from '../../../../@types/messageHandler';
import { Episode, QueueItem } from '../../../../@types/messageHandler';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
export type DownloadOptions = {
@ -14,6 +14,7 @@ export type DownloadOptions = {
export type StoreState = {
queue: QueueItem[],
episodeListing: Episode[];
downloadOptions: DownloadOptions,
service: 'crunchy'|'funi'|undefined
}
@ -43,7 +44,8 @@ const initialState: StoreState = {
all: false,
but: false
},
service: undefined
service: undefined,
episodeListing: []
};
const Store: React.FC = ({children}) => {