mirror of
https://github.com/anidl/multi-downloader-nx.git
synced 2026-01-11 20:10:20 +00:00
Merge branch 'v5' into remove-old-hd-api
This commit is contained in:
commit
7107cef7a4
22 changed files with 3575 additions and 8515 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
30
@types/mpd-parser.d.ts
vendored
30
@types/mpd-parser.d.ts
vendored
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
123
crunchy.ts
123
crunchy.ts
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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
3
gui/react/.babelrc
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env","@babel/preset-react", "@babel/preset-typescript"]
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10491
gui/react/pnpm-lock.yaml
10491
gui/react/pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -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/>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
47
gui/react/webpack.config.ts
Normal file
47
gui/react/webpack.config.ts
Normal 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;
|
||||
24
hidive.ts
24
hidive.ts
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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
144
modules/module.fetch.ts
Normal 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;
|
||||
}
|
||||
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
})
|
||||
|
|
|
|||
40
package.json
40
package.json
|
|
@ -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",
|
||||
|
|
|
|||
946
pnpm-lock.yaml
946
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
9
tsc.ts
9
tsc.ts
|
|
@ -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*',
|
||||
|
|
|
|||
Loading…
Reference in a new issue