Merge branch 'v5' into remove-old-hd-api

This commit is contained in:
AnimeDL 2024-04-13 07:36:32 -07:00 committed by GitHub
commit 7107cef7a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 3575 additions and 8515 deletions

11
.gitignore vendored
View file

@ -4,6 +4,8 @@
**/node_modules/
/videos/*.json
/videos/*.ts
/videos/*.m4s
/videos/*.txt
.DS_Store
ffmpeg
mkvmerge
@ -19,12 +21,9 @@ token.yml
lib
test.*
updates.json
funi_token.yml
cr_token.yml
hd_profile.yml
hd_sess.yml
hd_token.yml
hd_new_token.yml
*_token.yml
*_profile.yml
*_sess.yml
archive.json
guistate.json
fonts

View file

@ -7,11 +7,40 @@ declare module 'mpd-parser' {
map: {
uri: string,
resolvedUri: string,
byterange?: {
length: number,
offset: number
}
},
byterange?: {
length: number,
offset: number
},
number: number,
presentationTime: number
}
export type Sidx = {
uri: string,
resolvedUri: string,
byterange: {
length: number,
offset: number
},
map: {
uri: string,
resolvedUri: string,
byterange: {
length: number,
offset: number
}
},
duration: number,
timeline: number,
presentationTime: number,
number: number
}
export type Playlist = {
attributes: {
NAME: string,
@ -45,6 +74,7 @@ declare module 'mpd-parser' {
}
}
segments: Segment[]
sidx?: Sidx
}
export type Manifest = {

View file

@ -1,5 +1,24 @@
# Set the quality of the stream, 0 is highest available.
q: 0
nServer: 1
mp4mux: false
noCleanUp: false
dlVideoOnce: false
# Set which stream to use
kstream: 1
# Set which server to use
server: 1
# How many parts to download at once. Increasing may improve download speed.
partsize: 10
# Set whether to mux into an mp4 or not. Not recommended.
mp4: false
# Whether to delete any created files or not
nocleanup: false
# Whether to only download the relevant video once
dlVideoOnce: false
# Whether to keep all downloaded videos or only a single copy
keepAllVideos: false
# What to use as the file name template
fileName: "[${service}] ${showTitle} - S${season}E${episode} [${height}p]"
# What Audio languages to download
dubLang: ["jpn"]
# What Subtitle languages to download
dlsubs: ["all"]
# What language Audio to set as default
defaultAudio: "jpn"

View file

@ -25,7 +25,7 @@ import getKeys, { canDecrypt } from './modules/widevine';
// load req
import { domain, api } from './modules/module.api-urls';
import * as reqModule from './modules/module.req';
import * as reqModule from './modules/module.fetch';
import { CrunchySearch } from './@types/crunchySearch';
import { CrunchyEpisodeList, CrunchyEpisode } from './@types/crunchyEpisodeList';
import { CrunchyDownloadOptions, CrunchyEpMeta, CrunchyMuxOptions, CrunchyMultiDownload, DownloadedMedia, ParseItem, SeriesSearch, SeriesSearchItem } from './@types/crunchyTypes';
@ -210,9 +210,9 @@ export default class Crunchy implements ServiceClass {
console.info('');
}
const fontUrl = fontsData.root + f;
const getFont = await this.req.getData<Buffer>(fontUrl, { binary: true });
const getFont = await this.req.getData(fontUrl);
if(getFont.ok && getFont.res){
fs.writeFileSync(fontLoc, getFont.res.body);
fs.writeFileSync(fontLoc, Buffer.from(await getFont.res.arrayBuffer()));
console.info(`Downloaded: ${f}`);
}
else{
@ -240,7 +240,7 @@ export default class Crunchy implements ServiceClass {
console.error('Authentication failed!');
return { isOk: false, reason: new Error('Authentication failed') };
}
this.token = JSON.parse(authReq.res.body);
this.token = await authReq.res.json();
this.token.expires = new Date(Date.now() + this.token.expires_in);
yamlCfg.saveCRToken(this.token);
await this.getProfile();
@ -263,7 +263,7 @@ export default class Crunchy implements ServiceClass {
console.error('Authentication failed!');
return;
}
this.token = JSON.parse(authReq.res.body);
this.token = await authReq.res.json();
this.token.expires = new Date(Date.now() + this.token.expires_in);
yamlCfg.saveCRToken(this.token);
}
@ -284,7 +284,7 @@ export default class Crunchy implements ServiceClass {
console.error('Get profile failed!');
return false;
}
const profile = JSON.parse(profileReq.res.body);
const profile = await profileReq.res.json();
if (!silent) {
console.info('USER: %s (%s)', profile.username, profile.email);
}
@ -313,7 +313,7 @@ export default class Crunchy implements ServiceClass {
console.error('Token Authentication failed!');
return;
}
this.token = JSON.parse(authReq.res.body);
this.token = await authReq.res.json();
this.token.expires = new Date(Date.now() + this.token.expires_in);
yamlCfg.saveCRToken(this.token);
await this.getProfile(false);
@ -347,7 +347,7 @@ export default class Crunchy implements ServiceClass {
console.error('Authentication failed!');
return;
}
this.token = JSON.parse(authReq.res.body);
this.token = await authReq.res.json();
this.token.expires = new Date(Date.now() + this.token.expires_in);
yamlCfg.saveCRToken(this.token);
}
@ -382,7 +382,7 @@ export default class Crunchy implements ServiceClass {
console.error('Authentication CMS token failed!');
return;
}
this.cmsToken = JSON.parse(cmsTokenReq.res.body);
this.cmsToken = await cmsTokenReq.res.json();
console.info('Your Country: %s\n', this.cmsToken.cms?.bucket.split('/')[1]);
}
@ -411,7 +411,7 @@ export default class Crunchy implements ServiceClass {
console.error('Get CMS index FAILED!');
return;
}
console.info(JSON.parse(indexReq.res.body));
console.info(await indexReq.res.json());
}
public async doSearch(data: SearchData): Promise<SearchResponse>{
@ -438,7 +438,7 @@ export default class Crunchy implements ServiceClass {
console.error('Search FAILED!');
return { isOk: false, reason: new Error('Search failed. No more information provided') };
}
const searchResults = JSON.parse(searchReq.res.body) as CrunchySearch;
const searchResults = await searchReq.res.json() as CrunchySearch;
if(searchResults.total < 1){
console.info('Nothing Found!');
return { isOk: true, value: [] };
@ -699,7 +699,7 @@ export default class Crunchy implements ServiceClass {
console.error('Series Request FAILED!');
return;
}
const seriesData = JSON.parse(seriesReq.res.body);
const seriesData = await seriesReq.res.json();
await this.logObject(seriesData.data[0], pad, false);
}
// seasons list
@ -709,7 +709,7 @@ export default class Crunchy implements ServiceClass {
return;
}
// parse data
const seasonsList = JSON.parse(seriesSeasonListReq.res.body) as SeriesSearch;
const seasonsList = await seriesSeasonListReq.res.json() as SeriesSearch;
if(seasonsList.total < 1){
console.info('Series is empty!');
return;
@ -740,7 +740,7 @@ export default class Crunchy implements ServiceClass {
console.error('Movie Listing Request FAILED!');
return;
}
const movieListing = JSON.parse(movieListingReq.res.body);
const movieListing = await movieListingReq.res.json();
if(movieListing.total < 1){
console.info('Movie Listing is empty!');
return;
@ -755,7 +755,7 @@ export default class Crunchy implements ServiceClass {
console.error('Movies List Request FAILED!');
return;
}
const moviesList = JSON.parse(moviesListReq.res.body);
const moviesList = await moviesListReq.res.json();
for(const item of moviesList.data){
this.logObject(item, pad+2);
}
@ -782,7 +782,7 @@ export default class Crunchy implements ServiceClass {
console.error('Get newly added FAILED!');
return;
}
const newlyAddedResults = JSON.parse(newlyAddedReq.res.body);
const newlyAddedResults = await newlyAddedReq.res.json();
console.info('Newly added:');
for(const i of newlyAddedResults.items){
await this.logObject(i, 2);
@ -814,7 +814,7 @@ export default class Crunchy implements ServiceClass {
console.error('Show Request FAILED!');
return { isOk: false, reason: new Error('Show request failed. No more information provided.') };
}
const showInfo = JSON.parse(showInfoReq.res.body);
const showInfo = await showInfoReq.res.json();
this.logObject(showInfo.data[0], 0);
let episodeList = { total: 0, data: [], meta: {} } as CrunchyEpisodeList;
@ -840,7 +840,7 @@ export default class Crunchy implements ServiceClass {
return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') };
}
//CrunchyEpisodeList
const episodeListAndroid = JSON.parse(reqEpsList.res.body) as CrunchyAndroidEpisodes;
const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes;
episodeList = {
total: episodeListAndroid.total,
data: episodeListAndroid.items,
@ -853,7 +853,7 @@ export default class Crunchy implements ServiceClass {
return { isOk: false, reason: new Error('Episode List request failed. No more information provided.') };
}
//CrunchyEpisodeList
episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
episodeList = await reqEpsList.res.json() as CrunchyEpisodeList;
}
const epNumList: {
@ -1014,7 +1014,7 @@ export default class Crunchy implements ServiceClass {
continue;
}
const oldObjectInfo = JSON.parse(extIdReq.res.body) as Record<any, any>;
const oldObjectInfo = await extIdReq.res.json() as Record<any, any>;
for (const object of oldObjectInfo.items) {
objectIds.push(object.id);
}
@ -1061,14 +1061,14 @@ export default class Crunchy implements ServiceClass {
if(!objectReq.ok || !objectReq.res){
console.error('Objects Request FAILED!');
if(objectReq.error && objectReq.error.res && objectReq.error.res.body){
const objectInfo = JSON.parse(objectReq.error.res.body as string);
const objectInfo = await objectReq.error.res.json();
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
objectInfo.error = true;
return objectInfo;
}
return [];
}
const objectInfoAndroid = JSON.parse(objectReq.res.body) as CrunchyAndroidObject;
const objectInfoAndroid = await objectReq.res.json() as CrunchyAndroidObject;
objectInfo = {
total: objectInfoAndroid.total,
data: objectInfoAndroid.items,
@ -1079,14 +1079,14 @@ export default class Crunchy implements ServiceClass {
if(!objectReq.ok || !objectReq.res){
console.error('Objects Request FAILED!');
if(objectReq.error && objectReq.error.res && objectReq.error.res.body){
const objectInfo = JSON.parse(objectReq.error.res.body as string);
const objectInfo = await objectReq.error.res.json();
console.info('Body:', JSON.stringify(objectInfo, null, '\t'));
objectInfo.error = true;
return objectInfo;
}
return [];
}
objectInfo = JSON.parse(objectReq.res.body) as ObjectInfo;
objectInfo = await objectReq.res.json() as ObjectInfo;
}
if(earlyReturn){
@ -1201,6 +1201,9 @@ export default class Crunchy implements ServiceClass {
for (const mMeta of medias.data) {
console.info(`Requesting: [${mMeta.mediaId}] ${mediaName}`);
// Make sure we have a media id without a : in it
const currentMediaId = (mMeta.mediaId.includes(':') ? mMeta.mediaId.split(':')[1] : mMeta.mediaId);
//Make sure token is up to date
await this.refreshToken(true, true);
let currentVersion;
@ -1239,16 +1242,16 @@ export default class Crunchy implements ServiceClass {
const compiledChapters: string[] = [];
if (options.chapters) {
//Make Chapter Request
const chapterRequest = await this.req.getData(`https://static.crunchyroll.com/skip-events/production/${mMeta.mediaId}.json`);
const chapterRequest = await this.req.getData(`https://static.crunchyroll.com/skip-events/production/${currentMediaId}.json`);
if(!chapterRequest.ok || !chapterRequest.res){
//Old Chapter Request Fallback
console.warn('Chapter request failed, attempting old API');
const oldChapterRequest = await this.req.getData(`https://static.crunchyroll.com/datalab-intro-v2/${mMeta.mediaId}.json`);
const oldChapterRequest = await this.req.getData(`https://static.crunchyroll.com/datalab-intro-v2/${currentMediaId}.json`);
if(!oldChapterRequest.ok || !oldChapterRequest.res) {
console.warn('Old Chapter API request failed');
} else {
console.info('Old Chapter request successful');
const chapterData = JSON.parse(oldChapterRequest.res.body) as CrunchyOldChapter;
const chapterData = await oldChapterRequest.res.json() as CrunchyOldChapter;
//Generate Timestamps
const startTime = new Date(0), endTime = new Date(0);
@ -1278,7 +1281,7 @@ export default class Crunchy implements ServiceClass {
} else {
//Chapter request succeeded, now let's parse them
console.info('Chapter request successful');
const chapterData = JSON.parse(chapterRequest.res.body) as CrunchyChapters;
const chapterData = await chapterRequest.res.json() as CrunchyChapters;
const chapters: CrunchyChapter[] = [];
//Make a format more usable for the crunchy chapters
@ -1290,6 +1293,14 @@ export default class Crunchy implements ServiceClass {
if (chapters.length > 0) {
chapters.sort((a, b) => a.start - b.start);
//Check if chapters has an intro
if (!(chapters.find(c => c.type === 'intro') || chapters.find(c => c.type === 'recap'))) {
compiledChapters.push(
`CHAPTER${(compiledChapters.length/2)+1}=00:00:00.00`,
`CHAPTER${(compiledChapters.length/2)+1}NAME=Episode`
);
}
//Loop through all the chapters
for (const chapter of chapters) {
if (typeof chapter.start == 'undefined' || typeof chapter.end == 'undefined') continue;
@ -1370,7 +1381,7 @@ export default class Crunchy implements ServiceClass {
return undefined;
}
}
const pbDataAndroid = JSON.parse(playbackReq.res.body) as CrunchyAndroidStreams;
const pbDataAndroid = await playbackReq.res.json() as CrunchyAndroidStreams;
pbData = {
total: 0,
data: [pbDataAndroid.streams],
@ -1394,14 +1405,14 @@ export default class Crunchy implements ServiceClass {
return undefined;
}
}
pbData = JSON.parse(playbackReq.res.body) as PlaybackData;
pbData = await playbackReq.res.json() as PlaybackData;
}
const playbackReq = await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/${currentVersion ? currentVersion.guid : mMeta.mediaId}/console/switch/play`, AuthHeaders);
const playbackReq = await this.req.getData(`https://cr-play-service.prd.crunchyrollsvc.com/v1/${currentVersion ? currentVersion.guid : currentMediaId}/console/switch/play`, AuthHeaders);
if(!playbackReq.ok || !playbackReq.res){
console.error('Non-DRM Request Stream URLs FAILED!');
} else {
const playStream = JSON.parse(playbackReq.res.body) as CrunchyPlayStream;
const playStream = await playbackReq.res.json() as CrunchyPlayStream;
const derivedPlaystreams = {} as CrunchyStreams;
for (const hardsub in playStream.hardSubs) {
const stream = playStream.hardSubs[hardsub];
@ -1549,9 +1560,10 @@ export default class Crunchy implements ServiceClass {
console.error('CAN\'T FETCH VIDEO PLAYLISTS!');
dlFailed = true;
} else {
if (streamPlaylistsReq.res.body.match('MPD')) {
const streamPlaylistBody = await streamPlaylistsReq.res.text();
if (streamPlaylistBody.match('MPD')) {
//Parse MPD Playlists
const streamPlaylists = parse(streamPlaylistsReq.res.body, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), curStream.url.match(/.*\.urlset\//)[0]);
const streamPlaylists = await parse(streamPlaylistBody, langsData.findLang(langsData.fixLanguageTag(pbData.meta.audio_locale as string) || ''), curStream.url.match(/.*\.urlset\//)[0]);
//Get name of CDNs/Servers
const streamServers = Object.keys(streamPlaylists);
@ -1626,6 +1638,8 @@ export default class Crunchy implements ServiceClass {
// TODO check filename
fileName = parseFileName(options.fileName, variables, options.numbers, options.override).join(path.sep);
const outFile = parseFileName(options.fileName + '.' + (mMeta.lang?.name || lang.name), variables, options.numbers, options.override).join(path.sep);
const tempFile = parseFileName(`temp-${currentVersion ? currentVersion.guid : currentMediaId}`, variables, options.numbers, options.override).join(path.sep);
const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile);
let [audioDownloaded, videoDownloaded] = [false, false];
@ -1651,7 +1665,7 @@ export default class Crunchy implements ServiceClass {
segments: chosenVideoSegments.segments
};
const videoDownload = await new streamdl({
output: chosenVideoSegments.pssh ? `${tsFile}.video.enc.m4s` : `${tsFile}.video.m4s`,
output: chosenVideoSegments.pssh ? `${tempTsFile}.video.enc.m4s` : `${tsFile}.video.m4s`,
timeout: options.timeout,
m3u8json: videoJson,
// baseurl: chunkPlaylist.baseUrl,
@ -1693,7 +1707,7 @@ export default class Crunchy implements ServiceClass {
segments: chosenAudioSegments.segments
};
const audioDownload = await new streamdl({
output: chosenAudioSegments.pssh ? `${tsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`,
output: chosenAudioSegments.pssh ? `${tempTsFile}.audio.enc.m4s` : `${tsFile}.audio.m4s`,
timeout: options.timeout,
m3u8json: audioJson,
// baseurl: chunkPlaylist.baseUrl,
@ -1736,10 +1750,10 @@ export default class Crunchy implements ServiceClass {
})
});
if(!decReq.ok || !decReq.res){
console.error('Request to DRM Authentication failed:', decReq.error?.code, decReq.error?.message);
console.error('Request to DRM Authentication failed:', decReq.error?.res.status, decReq.error?.message);
return undefined;
}
const authData = JSON.parse(decReq.res.body) as {'custom_data': string, 'token': string};
const authData = await decReq.res.json() as {'custom_data': string, 'token': string};
const encryptionKeys = await getKeys(chosenVideoSegments.pssh, 'https://lic.drmtoday.com/license-proxy-widevine/cenc/', {
'dt-custom-data': authData.custom_data,
'x-dt-auth-token': authData.token
@ -1755,8 +1769,8 @@ export default class Crunchy implements ServiceClass {
if (this.cfg.bin.mp4decrypt) {
const commandBase = `--show-progress --key ${encryptionKeys[1].kid}:${encryptionKeys[1].key} `;
const commandVideo = commandBase+`"${tsFile}.video.enc.m4s" "${tsFile}.video.m4s"`;
const commandAudio = commandBase+`"${tsFile}.audio.enc.m4s" "${tsFile}.audio.m4s"`;
const commandVideo = commandBase+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`;
const commandAudio = commandBase+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`;
if (videoDownloaded) {
console.info('Started decrypting video');
@ -1764,12 +1778,14 @@ export default class Crunchy implements ServiceClass {
if (!decryptVideo.isOk) {
console.error(decryptVideo.err);
console.error(`Decryption failed with exit code ${decryptVideo.err.code}`);
fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`);
return undefined;
} else {
console.info('Decryption done for video');
if (!options.nocleanup) {
fs.removeSync(`${tsFile}.video.enc.m4s`);
fs.removeSync(`${tempTsFile}.video.enc.m4s`);
}
fs.renameSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`);
files.push({
type: 'Video',
path: `${tsFile}.video.m4s`,
@ -1785,11 +1801,13 @@ export default class Crunchy implements ServiceClass {
if (!decryptAudio.isOk) {
console.error(decryptAudio.err);
console.error(`Decryption failed with exit code ${decryptAudio.err.code}`);
fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`);
return undefined;
} else {
if (!options.nocleanup) {
fs.removeSync(`${tsFile}.audio.enc.m4s`);
fs.removeSync(`${tempTsFile}.audio.enc.m4s`);
}
fs.renameSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`);
files.push({
type: 'Audio',
path: `${tsFile}.audio.m4s`,
@ -1821,7 +1839,7 @@ export default class Crunchy implements ServiceClass {
}
}
} else if (!options.novids) {
const streamPlaylists = m3u8(streamPlaylistsReq.res.body);
const streamPlaylists = m3u8(streamPlaylistBody);
const plServerList: string[] = [],
plStreams: Record<string, Record<string, string>> = {},
plQuality: {
@ -1928,7 +1946,8 @@ export default class Crunchy implements ServiceClass {
console.error('CAN\'T FETCH VIDEO PLAYLIST!');
dlFailed = true;
} else {
const chunkPlaylist = m3u8(chunkPage.res.body);
const chunkPageBody = await chunkPage.res.text();
const chunkPlaylist = m3u8(chunkPageBody);
const totalParts = chunkPlaylist.segments.length;
const mathParts = Math.ceil(totalParts / options.partsize);
const mathMsg = `(${mathParts}*${options.partsize})`;
@ -2068,15 +2087,15 @@ export default class Crunchy implements ServiceClass {
if(options.dlsubs.includes('all') || options.dlsubs.includes(langItem.locale)){
const subsAssReq = await this.req.getData(subsItem.url);
if(subsAssReq.ok && subsAssReq.res){
let sBody;
let sBody = await subsAssReq.res.text();
if (subsItem.format == 'vtt') {
const chosenFontSize = options.originalFontSize ? undefined : options.fontSize;
if (!options.originalFontSize) subsAssReq.res.body = subsAssReq.res.body.replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, '');
sBody = vtt2ass(undefined, chosenFontSize, subsAssReq.res.body, '', undefined, options.fontName);
if (!options.originalFontSize) sBody = sBody.replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, '');
sBody = vtt2ass(undefined, chosenFontSize, sBody, '', undefined, options.fontName);
sxData.fonts = fontsData.assFonts(sBody) as Font[];
sxData.file = sxData.file.replace('.vtt','.ass');
} else {
sBody = '\ufeff' + subsAssReq.res.body;
sBody = '\ufeff' + sBody;
const sBodySplit = sBody.split('\r\n');
sBodySplit.splice(2, 0, 'ScaledBorderAndShadow: yes');
sBody = sBodySplit.join('\r\n');
@ -2467,7 +2486,7 @@ export default class Crunchy implements ServiceClass {
return;
}
// parse data
const seasonsList = JSON.parse(seriesSeasonListReq.res.body) as SeriesSearch;
const seasonsList = await seriesSeasonListReq.res.json() as SeriesSearch;
if(seasonsList.total < 1){
console.info('Series is empty!');
return;
@ -2494,7 +2513,7 @@ export default class Crunchy implements ServiceClass {
console.error('Show Request FAILED!');
return;
}
const showInfo = JSON.parse(showInfoReq.res.body);
const showInfo = await showInfoReq.res.json();
if (log)
this.logObject(showInfo, 0);
@ -2521,7 +2540,7 @@ export default class Crunchy implements ServiceClass {
return;
}
//CrunchyEpisodeList
const episodeListAndroid = JSON.parse(reqEpsList.res.body) as CrunchyAndroidEpisodes;
const episodeListAndroid = await reqEpsList.res.json() as CrunchyAndroidEpisodes;
episodeList = {
total: episodeListAndroid.total,
data: episodeListAndroid.items,
@ -2534,7 +2553,7 @@ export default class Crunchy implements ServiceClass {
return;
}
//CrunchyEpisodeList
episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
episodeList = await reqEpsList.res.json() as CrunchyEpisodeList;
}
if(episodeList.total < 1){

View file

@ -1,4 +1,4 @@
# multi-downloader-nx (4.6.3v)
# multi-downloader-nx (4.7.1v)
If you find any bugs in this documentation or in the program itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).

View file

@ -60,20 +60,40 @@ AniDL --service {ServiceName} -s {SeasonID} -e {EpisodeNumber}
Dependencies that are only required for running from code. These are not required if you are using the prebuilt binaries.
* NodeJS >= 14.6.0 (https://nodejs.org/)
* NodeJS >= 18.0.0 (https://nodejs.org/)
* NPM >= 6.9.0 (https://www.npmjs.org/)
* PNPM >= 7.0.0 (https://pnpm.io/)
### Build Instructions
### Build Setup
Please note that NodeJS, NPM, and PNPM must be installed on your system. For instructions on how to install pnpm, check (https://pnpm.io/installation)
First clone this repo `git clone https://github.com/anidl/multi-downloader-nx.git`.
`cd` into the cloned directory and run `pnpm i`.
Afterwards run `pnpm run tsc false [true if you want gui, false otherwise]`.
`cd` into the cloned directory and run `pnpm i`. Next, decide if you want to package the application, build the code, or run from typescript.
If you want the `js` files you are done. Just `cd` into the `lib` folder, and run `node index.js --help` to get started with the CLI, or run `node gui.js` to run the GUI
### Run from TypeScript
You can run the code from native TypeScript, this requires ts-node which you can install with pnpm with the following command: `pnpm -g i ts-node`
Afterwords, you can run the application like this:
* CLI: `ts-node -T ./index.ts --help`
* GUI: `ts-node -T ./gui.ts`
### Run as JavaScript
If you want to build the application into JavaScript code to run, you can do that as well like this:
* CLI: `pnpm run prebuild-cli`
* GUI: `pnpm run prebuild-gui`
Then you can cd into the `lib` folder and you will be able to run the CLI or GUI as follows:
* CLI: `node ./index.js --help`
* GUI: `node ./gui.js`
### Build the application into an executable
If you want to package the application, run pnpm run build-`{platform}`-`{type}` where `{platform}` is the operating system (currently the choices are windows, linux, macos, alpine, android, and arm) and `{type}` is cli or gui.

3
gui/react/.babelrc Normal file
View file

@ -0,0 +1,3 @@
{
"presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"]
}

View file

@ -1,39 +1,42 @@
{
"name": "react",
"version": "0.1.0",
"name": "anidl-gui",
"version": "1.0.0",
"private": true,
"dependencies": {
"@babel/core": ">=7.0.0-0 <8.0.0",
"@babel/plugin-syntax-flow": "^7.14.5",
"@babel/plugin-transform-react-jsx": "^7.14.9",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.11.9",
"@mui/lab": "^5.0.0-alpha.120",
"@mui/material": "^5.11.9",
"@types/node": "^18.14.0",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.11",
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.15",
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.15",
"notistack": "^2.0.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"uuid": "^9.0.0",
"ws": "^8.12.1"
"typescript": "^5.4.4",
"uuid": "^9.0.1",
"ws": "^8.16.0"
},
"devDependencies": {
"@babel/cli": "^7.24.1",
"@babel/core": "^7.24.4",
"@babel/preset-env": "^7.24.4",
"@babel/preset-react": "^7.24.1",
"@babel/preset-typescript": "^7.24.1",
"@types/node": "^18.14.0",
"@types/react": "^18.2.74",
"@types/react-dom": "^18.2.24",
"@types/uuid": "^9.0.8",
"babel-loader": "^9.1.3",
"css-loader": "^7.0.0",
"html-webpack-plugin": "^5.6.0",
"style-loader": "^3.3.4",
"ts-node": "^10.9.2",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
},
"proxy": "http://localhost:3000",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"build": "npx tsc && npx webpack"
},
"browserslist": {
"production": [
@ -46,8 +49,5 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/uuid": "^9.0.1"
}
}

File diff suppressed because it is too large Load diff

View file

@ -27,7 +27,7 @@ export default class ErrorHandler extends React.Component<{
<Typography variant='body1' color='red'>
{`${this.state.error.er.name}: ${this.state.error.er.message}`}
<br/>
{this.state.error.stack.componentStack.split('\n').map(a => {
{this.state.error.stack.componentStack?.split('\n').map(a => {
return <>
{a}
<br/>

View file

@ -1,5 +1,6 @@
{
"compilerOptions": {
"outDir": "./build",
"target": "es5",
"lib": [
"dom",
@ -13,15 +14,16 @@
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"module": "CommonJS",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
//"noEmit": true,
"jsx": "react-jsx",
"downlevelIteration": true
},
"include": [
"./src"
"./src",
"./webpack.config.ts"
]
}

View file

@ -0,0 +1,47 @@
import webpack from 'webpack';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import path from 'path';
const config: webpack.Configuration = {
entry: './src/index.tsx',
mode: 'production',
output: {
path: path.resolve(__dirname, './build'),
filename: 'index.js',
},
target: 'web',
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
},
module: {
rules: [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: {
'loader': 'babel-loader',
options: {
presets: [
'@babel/typescript',
'@babel/preset-react',
['@babel/preset-env', {
targets: 'defaults'
}]
]
}
},
},
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, 'public', 'index.html')
})
]
};
export default config;

View file

@ -538,7 +538,7 @@ export default class Hidive implements ServiceClass {
console.info('\tSubs : ' + availableSubs.map(a => langsData.languages.find(b => b.new_hd_locale == a.language)?.name).join('\n\t\t'));
console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`);
const baseUrl = playbackData.dash[0].url.split('master')[0];
const parsedmpd = parse(mpd, undefined, baseUrl);
const parsedmpd = await parse(mpd, undefined, baseUrl);
const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options);
if (res === undefined || res.error) {
console.error('Failed to download media list');
@ -627,7 +627,7 @@ export default class Hidive implements ServiceClass {
console.info('\tSubs : ' + availableSubs.map(a => langsData.languages.find(b => b.new_hd_locale == a.language)?.name).join('\n\t\t'));
console.info(`[INFO] Selected dub(s): ${options.dubLang.join(', ')}`);
const baseUrl = playbackData.dash[0].url.split('master')[0];
const parsedmpd = parse(mpd, undefined, baseUrl);
const parsedmpd = await parse(mpd, undefined, baseUrl);
const res = await this.downloadMPD(parsedmpd, availableSubs, selectedEpisode, options);
if (res === undefined || res.error) {
console.error('Failed to download media list');
@ -775,6 +775,8 @@ export default class Hidive implements ServiceClass {
const mathMsg = `(${mathParts}*${options.partsize})`;
console.info('Total parts in video stream:', totalParts, mathMsg);
const tsFile = path.isAbsolute(fileName) ? fileName : path.join(this.cfg.dir.content, fileName);
const tempFile = parseFileName(`temp-${selectedEpisode.id}`, variables, options.numbers, options.override).join(path.sep);
const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile);
const split = fileName.split(path.sep).slice(0, -1);
split.forEach((val, ind, arr) => {
const isAbsolut = path.isAbsolute(fileName);
@ -785,7 +787,7 @@ export default class Hidive implements ServiceClass {
segments: chosenVideoSegments.segments
};
const videoDownload = await new streamdl({
output: `${tsFile}.video.enc.m4s`,
output: `${tempTsFile}.video.enc.m4s`,
timeout: options.timeout,
m3u8json: videoJson,
// baseurl: chunkPlaylist.baseUrl,
@ -814,19 +816,21 @@ export default class Hidive implements ServiceClass {
}
if (this.cfg.bin.mp4decrypt) {
const commandBase = `--show-progress --key ${encryptionKeys[1].kid}:${encryptionKeys[1].key} `;
const commandVideo = commandBase+`"${tsFile}.video.enc.m4s" "${tsFile}.video.m4s"`;
const commandVideo = commandBase+`"${tempTsFile}.video.enc.m4s" "${tempTsFile}.video.m4s"`;
console.info('Started decrypting video');
const decryptVideo = exec('mp4decrypt', `"${this.cfg.bin.mp4decrypt}"`, commandVideo);
if (!decryptVideo.isOk) {
console.error(decryptVideo.err);
console.error(`Decryption failed with exit code ${decryptVideo.err.code}`);
fs.renameSync(`${tempTsFile}.video.enc.m4s`, `${tsFile}.video.enc.m4s`);
return undefined;
} else {
console.info('Decryption done for video');
if (!options.nocleanup) {
fs.removeSync(`${tsFile}.video.enc.m4s`);
fs.removeSync(`${tempTsFile}.video.enc.m4s`);
}
fs.renameSync(`${tempTsFile}.video.m4s`, `${tsFile}.video.m4s`);
files.push({
type: 'Video',
path: `${tsFile}.video.m4s`,
@ -851,6 +855,8 @@ export default class Hidive implements ServiceClass {
const mathParts = Math.ceil(totalParts / options.partsize);
const mathMsg = `(${mathParts}*${options.partsize})`;
console.info('Total parts in audio stream:', totalParts, mathMsg);
const tempFile = parseFileName(`temp-${selectedEpisode.id}.${chosenAudioSegments.language.name}`, variables, options.numbers, options.override).join(path.sep);
const tempTsFile = path.isAbsolute(tempFile as string) ? tempFile : path.join(this.cfg.dir.content, tempFile);
const outFile = parseFileName(options.fileName + '.' + (chosenAudioSegments.language.name), variables, options.numbers, options.override).join(path.sep);
const tsFile = path.isAbsolute(outFile as string) ? outFile : path.join(this.cfg.dir.content, outFile);
const split = outFile.split(path.sep).slice(0, -1);
@ -863,7 +869,7 @@ export default class Hidive implements ServiceClass {
segments: chosenAudioSegments.segments
};
const audioDownload = await new streamdl({
output: `${tsFile}.audio.enc.m4s`,
output: `${tempTsFile}.audio.enc.m4s`,
timeout: options.timeout,
m3u8json: audioJson,
// baseurl: chunkPlaylist.baseUrl,
@ -892,18 +898,20 @@ export default class Hidive implements ServiceClass {
}
if (this.cfg.bin.mp4decrypt) {
const commandBase = `--show-progress --key ${encryptionKeys[1].kid}:${encryptionKeys[1].key} `;
const commandAudio = commandBase+`"${tsFile}.audio.enc.m4s" "${tsFile}.audio.m4s"`;
const commandAudio = commandBase+`"${tempTsFile}.audio.enc.m4s" "${tempTsFile}.audio.m4s"`;
console.info('Started decrypting audio');
const decryptAudio = exec('mp4decrypt', `"${this.cfg.bin.mp4decrypt}"`, commandAudio);
if (!decryptAudio.isOk) {
console.error(decryptAudio.err);
console.error(`Decryption failed with exit code ${decryptAudio.err.code}`);
fs.renameSync(`${tempTsFile}.audio.enc.m4s`, `${tsFile}.audio.enc.m4s`);
return undefined;
} else {
if (!options.nocleanup) {
fs.removeSync(`${tsFile}.audio.enc.m4s`);
fs.removeSync(`${tempTsFile}.audio.enc.m4s`);
}
fs.renameSync(`${tempTsFile}.audio.m4s`, `${tsFile}.audio.m4s`);
files.push({
type: 'Audio',
path: `${tsFile}.audio.m4s`,

View file

@ -47,6 +47,7 @@ async function buildBinary(buildType: BuildTypes, gui: boolean) {
gui ? 'gui.js' : 'index.js',
'--target', nodeVer + buildType,
'--output', `${buildDir}/${pkg.short_name}`,
'--compress', 'GZip'
];
console.info(`[Build] Build configuration: ${buildFull}`);
try {

View file

@ -1,5 +1,3 @@
import { Headers } from 'got/dist/source';
// api domains
const domain = {
www: 'https://www.crunchyroll.com',
@ -34,8 +32,8 @@ export type APIType = {
cms: string
beta_browse: string
beta_cms: string,
beta_authHeader: Headers,
beta_authHeaderMob: Headers,
beta_authHeader: Record<string, string>,
beta_authHeaderMob: Record<string, string>,
hd_apikey: string,
hd_devName: string,
hd_appId: string,
@ -65,7 +63,7 @@ const api: APIType = {
// beta api
beta_auth: `${domain.api_beta}/auth/v1/token`,
beta_authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
beta_authBasicMob: 'Basic b2VkYXJteHN0bGgxanZhd2ltbnE6OWxFaHZIWkpEMzJqdVY1ZFc5Vk9TNTdkb3BkSnBnbzE=',
beta_authBasicMob: 'Basic bm12anNoZmtueW14eGtnN2ZiaDk6WllJVnJCV1VQYmNYRHRiRDIyVlNMYTZiNFdRb3Mzelg=',
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
beta_cmsToken: `${domain.api_beta}/index/v2`,
search: `${domain.api_beta}/content/v2/discover/search`,

View file

@ -29,7 +29,7 @@ const availableFilenameVars: AvailableFilenameVars[] = [
export type AvailableMuxer = 'ffmpeg' | 'mkvmerge'
export const muxer: AvailableMuxer[] = [ 'ffmpeg', 'mkvmerge' ];
type TAppArg<T extends boolean|string|number|unknown[], K = any> = {
export type TAppArg<T extends boolean|string|number|unknown[], K = any> = {
name: string,
group: keyof typeof groups,
type: 'boolean'|'string'|'number'|'array',
@ -867,7 +867,6 @@ const buildDefault = () => {
};
export {
TAppArg,
getDefault,
buildDefault,
args,

144
modules/module.fetch.ts Normal file
View file

@ -0,0 +1,144 @@
import * as yamlCfg from './module.cfg-loader';
import { console } from './log';
import { Method } from 'got';
export type Params = {
method?: Method,
headers?: Record<string, string>,
body?: string | Buffer,
binary?: boolean,
followRedirect?: 'follow' | 'error' | 'manual'
}
// req
export class Req {
private sessCfg: string;
private service: 'cr'|'funi'|'hd';
private session: Record<string, {
value: string;
expires: Date;
path: string;
domain: string;
secure: boolean;
'Max-Age'?: string
}> = {};
private cfgDir = yamlCfg.cfgDir;
private curl: boolean|string = false;
constructor(private domain: Record<string, unknown>, private debug: boolean, private nosess = false, private type: 'cr'|'funi'|'hd') {
this.sessCfg = yamlCfg.sessCfgFile[type];
this.service = type;
}
async getData(durl: string, params?: Params) {
params = params || {};
// options
const options: RequestInit = {
method: params.method ? params.method : 'GET',
headers: {
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
'accept-language': 'en-US,en;q=0.9',
'cache-control': 'no-cache',
'pragma': 'no-cache',
'sec-ch-ua': '"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Windows"',
'sec-fetch-dest': 'document',
'sec-fetch-mode': 'navigate',
'sec-fetch-site': 'none',
'sec-fetch-user': '?1',
'upgrade-insecure-requests': '1',
},
};
// additional params
if(params.headers){
options.headers = {...options.headers, ...params.headers};
}
if(options.method == 'POST'){
if (!(options.headers as Record<string, string>)['Content-Type']) {
(options.headers as Record<string, string>)['Content-Type'] = 'application/x-www-form-urlencoded';
}
}
if(params.body){
options.body = params.body;
}
if(typeof params.followRedirect == 'string'){
options.redirect = params.followRedirect;
}
// debug
if(this.debug){
console.debug('[DEBUG] GOT OPTIONS:');
console.debug(options);
}
// try do request
try {
const res = await fetch(durl.toString(), options);
if (!res.ok) {
console.error(`${res.status}: ${res.statusText}`);
const body = await res.text();
const docTitle = body.match(/<title>(.*)<\/title>/);
if(body && docTitle){
console.error(docTitle[1]);
}
}
return {
ok: res.ok,
res
};
}
catch(_error){
const error = _error as {
name: string
} & TypeError & {
res: Response
};
if (error.res && error.res.status && error.res.statusText) {
console.error(`${error.name} ${error.res.status}: ${error.res.statusText}`);
} else {
console.error(`${error.name}: ${error.res?.statusText || error.message}`);
}
if(error.res) {
const body = await error.res.text();
const docTitle = body.match(/<title>(.*)<\/title>/);
if(body && docTitle){
console.error(docTitle[1]);
}
}
return {
ok: false,
error,
};
}
}
}
export function buildProxy(proxyBaseUrl: string, proxyAuth: string){
if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){
proxyBaseUrl = 'http://' + proxyBaseUrl;
}
const proxyCfg = new URL(proxyBaseUrl);
let proxyStr = `${proxyCfg.protocol}//`;
if(typeof proxyCfg.hostname != 'string' || proxyCfg.hostname == ''){
throw new Error('[ERROR] Hostname and port required for proxy!');
}
if(proxyAuth && typeof proxyAuth == 'string' && proxyAuth.match(':')){
proxyCfg.username = proxyAuth.split(':')[0];
proxyCfg.password = proxyAuth.split(':')[1];
proxyStr += `${proxyCfg.username}:${proxyCfg.password}@`;
}
proxyStr += proxyCfg.hostname;
if(!proxyCfg.port && proxyCfg.protocol == 'http:'){
proxyStr += ':80';
}
else if(!proxyCfg.port && proxyCfg.protocol == 'https:'){
proxyStr += ':443';
}
return proxyStr;
}

View file

@ -75,7 +75,7 @@ class Merger {
for (const [vnaIndex, vna] of vnas.entries()) {
const streamInfo = await ffprobe(vna.path, { path: bin.ffprobe as string });
const videoInfo = streamInfo.streams.filter(stream => stream.codec_type == 'video');
vnas[vnaIndex].duration = videoInfo[0].duration;
vnas[vnaIndex].duration = parseInt(videoInfo[0].duration as string);
}
//Sort videoAndAudio streams by duration (shortest first)
vnas.sort((a,b) => {

View file

@ -7,9 +7,17 @@ type Segment = {
duration: number;
map: {
uri: string;
byterange?: {
length: number,
offset: number
};
};
number: number;
presentationTime: number;
byterange?: {
length: number,
offset: number
};
number?: number;
presentationTime?: number;
}
export type PlaylistItem = {
@ -38,19 +46,49 @@ export type MPDParsed = {
}
}
export function parse(manifest: string, language?: LanguageItem, url?: string) {
export async function parse(manifest: string, language?: LanguageItem, url?: string) {
if (!manifest.includes('BaseURL') && url) {
manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`);
}
const parsed = mpdParse(manifest);
const ret: MPDParsed = {};
// Audio Loop
for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)){
for (const playlist of item.playlists) {
const host = new URL(playlist.resolvedUri).hostname;
if (!Object.prototype.hasOwnProperty.call(ret, host))
ret[host] = { audio: [], video: [] };
if (playlist.sidx && playlist.segments.length == 0) {
const item = await fetch(playlist.sidx.uri, {
'method': 'head'
});
const byteLength = parseInt(item.headers.get('content-length') as string);
let currentByte = playlist.sidx.map.byterange.length;
while (currentByte <= byteLength) {
playlist.segments.push({
'duration': 0,
'map': {
'uri': playlist.resolvedUri,
'resolvedUri': playlist.resolvedUri,
'byterange': playlist.sidx.map.byterange
},
'uri': playlist.resolvedUri,
'resolvedUri': playlist.resolvedUri,
'byterange': {
'length': 500000,
'offset': currentByte
},
timeline: 0,
number: 0,
presentationTime: 0
});
currentByte = currentByte + 500000;
}
}
//Find and add audio language if it is found in the MPD
let audiolang: LanguageItem;
const foundlanguage = findLang(languages.find(a => a.code === item.language)?.cr_locale ?? 'unknown');
@ -68,10 +106,11 @@ export function parse(manifest: string, language?: LanguageItem, url?: string) {
const map_uri = segment.map.resolvedUri;
return {
duration: segment.duration,
map: { uri: map_uri },
map: { uri: map_uri, byterange: segment.map.byterange },
number: segment.number,
presentationTime: segment.presentationTime,
timeline: segment.timeline,
byterange: segment.byterange,
uri
};
})
@ -85,11 +124,40 @@ export function parse(manifest: string, language?: LanguageItem, url?: string) {
}
}
// Video Loop
for (const playlist of parsed.playlists) {
const host = new URL(playlist.resolvedUri).hostname;
if (!Object.prototype.hasOwnProperty.call(ret, host))
ret[host] = { audio: [], video: [] };
if (playlist.sidx && playlist.segments.length == 0) {
const item = await fetch(playlist.sidx.uri, {
'method': 'head'
});
const byteLength = parseInt(item.headers.get('content-length') as string);
let currentByte = playlist.sidx.map.byterange.length;
while (currentByte <= byteLength) {
playlist.segments.push({
'duration': 0,
'map': {
'uri': playlist.resolvedUri,
'resolvedUri': playlist.resolvedUri,
'byterange': playlist.sidx.map.byterange
},
'uri': playlist.resolvedUri,
'resolvedUri': playlist.resolvedUri,
'byterange': {
'length': 2000000,
'offset': currentByte
},
timeline: 0,
number: 0,
presentationTime: 0
});
currentByte = currentByte + 2000000;
}
}
const pItem: VideoPlayList = {
bandwidth: playlist.attributes.BANDWIDTH,
quality: playlist.attributes.RESOLUTION!,
@ -98,10 +166,11 @@ export function parse(manifest: string, language?: LanguageItem, url?: string) {
const map_uri = segment.map.resolvedUri;
return {
duration: segment.duration,
map: { uri: map_uri },
map: { uri: map_uri, byterange: segment.map.byterange },
number: segment.number,
presentationTime: segment.presentationTime,
timeline: segment.timeline,
byterange: segment.byterange,
uri
};
})

View file

@ -1,7 +1,7 @@
{
"name": "multi-downloader-nx",
"short_name": "aniDL",
"version": "4.6.3",
"version": "4.7.1",
"description": "Downloader for Crunchyroll, Funimation, or Hidive via CLI or GUI",
"keywords": [
"download",
@ -42,19 +42,12 @@
},
"license": "MIT",
"dependencies": {
"@babel/core": "^7.22.9",
"@babel/plugin-syntax-flow": "^7.22.5",
"@babel/plugin-transform-react-jsx": "^7.22.5",
"@types/xmldom": "^0.1.34",
"@yao-pkg/pkg": "^5.11.5",
"cheerio": "1.0.0-rc.12",
"@yao-pkg/pkg": "^5.11.1",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"eslint-plugin-import": "^2.29.1",
"express": "^4.18.2",
"express": "^4.19.2",
"ffprobe": "^1.1.2",
"form-data": "^4.0.0",
"fs-extra": "^11.1.1",
"fs-extra": "^11.2.0",
"got": "^11.8.6",
"iso-639": "^0.2.2",
"log4js": "^6.9.1",
@ -65,33 +58,30 @@
"open": "^8.4.2",
"protobufjs": "^7.2.6",
"sei-helper": "^3.3.0",
"typescript-eslint": "7.5.0",
"ws": "^8.13.0",
"xmldom": "^0.6.0",
"yaml": "^2.3.1",
"ws": "^8.16.0",
"yaml": "^2.4.1",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/cors": "^2.8.13",
"@types/express": "^4.17.17",
"@types/ffprobe": "^1.1.4",
"@types/fs-extra": "^11.0.1",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/ffprobe": "^1.1.8",
"@types/fs-extra": "^11.0.4",
"@types/node": "^18.15.11",
"@types/ws": "^8.5.5",
"@types/yargs": "^17.0.24",
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.32",
"@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0",
"@vercel/webpack-asset-relocator-loader": "^1.7.3",
"@yao-pkg/pkg": "^5.11.1",
"eslint": "^8.45.0",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-react": "7.34.1",
"protoc": "^1.1.3",
"removeNPMAbsolutePaths": "^3.0.1",
"ts-node": "^10.9.2",
"ts-proto": "^1.171.0",
"typescript": "5.4.4"
"typescript": "5.4.4",
"typescript-eslint": "7.5.0"
},
"scripts": {
"prestart": "pnpm run tsc test",

File diff suppressed because it is too large Load diff

9
tsc.ts
View file

@ -32,12 +32,9 @@ const ignore = [
'./bin/mkvtoolnix*',
'./config/token.yml$',
'./config/updates.json$',
'./config/cr_token.yml$',
'./config/funi_token.yml$',
'./config/new_hd_token.yml$',
'./config/hd_token.yml$',
'./config/hd_sess.yml$',
'./config/hd_profile.yml$',
'./config/*_token.yml$',
'./config/*_sess.yml$',
'./config/*_profile.yml$',
'*/\\.eslint*',
'*/*\\.tsx?$',
'./fonts*',