50% crunchy-beta.ts

This commit is contained in:
Izuco 2021-10-31 13:21:02 +01:00
parent 4a83046116
commit 58697e8b0f
No known key found for this signature in database
GPG key ID: 318460063D70949F
16 changed files with 737 additions and 381 deletions

122
@types/crunchyEpisodeList.d.ts vendored Normal file
View file

@ -0,0 +1,122 @@
export interface CrunchyEpisodeList {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Actions;
__actions__: Actions;
total: number;
items: Item[];
}
export interface Actions {
}
export interface Item {
__class__: Class;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: Actions;
id: string;
channel_id: ChannelID;
series_id: string;
series_title: string;
series_slug_title: string;
season_id: string;
season_title: string;
season_slug_title: string;
season_number: number;
episode: string;
episode_number: number | null;
sequence_number: number;
production_episode_id: string;
title: string;
slug_title: string;
description: string;
next_episode_id?: string;
next_episode_title?: string;
hd_flag: boolean;
is_mature: boolean;
mature_blocked: boolean;
episode_air_date: string;
is_subbed: boolean;
is_dubbed: boolean;
is_clip: boolean;
seo_title: string;
seo_description: string;
season_tags: string[];
available_offline: boolean;
media_type: Class;
slug: string;
images: Images;
duration_ms: number;
ad_breaks: AdBreak[];
is_premium_only: boolean;
listing_id: string;
subtitle_locales: SubtitleLocale[];
playback?: string;
availability_notes: string;
available_date?: string;
hide_season_title?: boolean;
hide_season_number?: boolean;
isSelected?: boolean;
seq_id: string;
}
export enum Class {
Episode = "episode",
}
export interface Links {
ads: Ads;
"episode/channel": Ads;
"episode/next_episode"?: Ads;
"episode/season": Ads;
"episode/series": Ads;
streams?: Ads;
}
export interface Ads {
href: string;
}
export interface AdBreak {
type: AdBreakType;
offset_ms: number;
}
export enum AdBreakType {
Midroll = "midroll",
Preroll = "preroll",
}
export enum ChannelID {
Crunchyroll = "crunchyroll",
}
export interface Images {
thumbnail: Array<Thumbnail[]>;
}
export interface Thumbnail {
width: number;
height: number;
type: ThumbnailType;
source: string;
}
export enum ThumbnailType {
Thumbnail = "thumbnail",
}
export enum SubtitleLocale {
ArSA = "ar-SA",
DeDE = "de-DE",
EnUS = "en-US",
Es419 = "es-419",
EsES = "es-ES",
FrFR = "fr-FR",
ItIT = "it-IT",
PtBR = "pt-BR",
RuRU = "ru-RU",
}

175
@types/crunchySearch.d.ts vendored Normal file
View file

@ -0,0 +1,175 @@
// Generated by https://quicktype.io
export interface CrunchySearch {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: CrunchySearchLinks;
__actions__: Actions;
total: number;
items: CrunchySearchItem[];
}
export interface Actions {
}
export interface CrunchySearchLinks {
continuation?: Continuation;
}
export interface Continuation {
href: string;
}
export interface CrunchySearchItem {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: CrunchySearchLinks;
__actions__: Actions;
type: string;
total: number;
items: ItemItem[];
}
export interface ItemItem {
__actions__: Actions;
__class__: Class;
__href__: string;
__links__: PurpleLinks;
channel_id: ChannelID;
description: string;
external_id: string;
id: string;
images: Images;
linked_resource_key: string;
new: boolean;
new_content: boolean;
promo_description: string;
promo_title: string;
search_metadata: SearchMetadata;
series_metadata?: SeriesMetadata;
slug: string;
slug_title: string;
title: string;
type: ItemType;
episode_metadata?: EpisodeMetadata;
playback?: string;
isSelected?: boolean;
season_number?: string;
is_premium_only?: boolean;
hide_metadata?: boolean;
seq_id?: string;
f_num?: string;
s_num?: string;
ep_num?: string;
last_public?: string;
subtitle_locales?: string[];
availability_notes?: string
}
export enum Class {
Panel = "panel",
}
export interface PurpleLinks {
resource: Continuation;
"resource/channel": Continuation;
"episode/season"?: Continuation;
"episode/series"?: Continuation;
streams?: Continuation;
}
export enum ChannelID {
Crunchyroll = "crunchyroll",
}
export interface EpisodeMetadata {
ad_breaks: AdBreak[];
availability_notes: string;
available_offline: boolean;
duration_ms: number;
episode: string;
episode_air_date: string;
episode_number: number;
is_clip: boolean;
is_dubbed: boolean;
is_mature: boolean;
is_premium_only: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_id: string;
season_number: number;
season_slug_title: string;
season_title: string;
sequence_number: number;
series_id: string;
series_slug_title: string;
series_title: string;
subtitle_locales: string[];
tenant_categories?: TenantCategory[];
available_date?: string;
free_available_date?: string;
}
export interface AdBreak {
offset_ms: number;
type: AdBreakType;
}
export enum AdBreakType {
Midroll = "midroll",
Preroll = "preroll",
}
export enum TenantCategory {
Action = "Action",
Drama = "Drama",
SciFi = "Sci-Fi",
}
export interface Images {
poster_tall?: Array<PosterTall[]>;
poster_wide?: Array<PosterTall[]>;
thumbnail?: Array<PosterTall[]>;
}
export interface PosterTall {
height: number;
source: string;
type: PosterTallType;
width: number;
}
export enum PosterTallType {
PosterTall = "poster_tall",
PosterWide = "poster_wide",
Thumbnail = "thumbnail",
}
export interface SearchMetadata {
score: number;
}
export interface SeriesMetadata {
availability_notes: string;
episode_count: number;
extended_description: string;
is_dubbed: boolean;
is_mature: boolean;
is_simulcast: boolean;
is_subbed: boolean;
mature_blocked: boolean;
maturity_ratings: string[];
season_count: number;
tenant_categories: TenantCategory[];
}
export enum ItemType {
Episode = "episode",
Series = "series",
Season = 'season',
MovieListing = 'movie_listing',
Movie = 'Movie'
}

26
@types/crunchyTypes.ts Normal file
View file

@ -0,0 +1,26 @@
export type CrunchyEpMeta = {
mediaId: string,
seasonTitle: string,
episodeNumber: string,
episodeTitle: string,
playback?: string
}
export type ParseItem = {
isSelected?: boolean,
type?: string,
id: string,
title: string,
playback?: string,
season_number?: number|string,
is_premium_only?: boolean,
hide_metadata?: boolean,
seq_id?: string,
f_num?: string,
s_num?: string
external_id?: string,
ep_num?: string
last_public?: string,
subtitle_locales?: string[],
availability_notes?: string
}

View file

@ -1,5 +1,15 @@
declare module 'sei-helper' {
export async function question(qStr: string): string;
export async function question(qStr: string): Promise<string>;
export function cleanupFilename(str: string): string;
export function exec(str: string, str1: string, str2: string);
export const cookie: {
parse: (data: Record<string, string>) => Record<string, {
value: string;
expires: Date;
path: string;
domain: string;
secure: boolean;
}>
}
export function formatTime(time: number): string
}

View file

@ -1,40 +1,44 @@
#!/usr/bin/env node
// build-in
const path = require('path');
const fs = require('fs-extra');
import path from 'path';
import fs from 'fs-extra';
// package program
const packageJson = require('./package.json');
import packageJson from './package.json';
console.log(`\n=== Crunchyroll Beta Downloader NX ${packageJson.version} ===\n`);
// plugins
const shlp = require('sei-helper');
const m3u8 = require('m3u8-parsed');
const streamdl = require('hls-download');
import shlp from 'sei-helper';
import m3u8 from 'm3u8-parsed';
import streamdl from 'hls-download';
// custom modules
const fontsData = require('./crunchy/modules/module.fontsData');
const langsData = require('./crunchy/modules/module.langsData');
const yamlCfg = require('./crunchy/modules/module.cfg-loader');
const yargs = require('./crunchy/modules/module.app-args');
const epsFilter = require('./crunchy/modules/module.eps-filter');
const appMux = require('./crunchy/modules/module.muxing');
import * as fontsData from './modules/module.fontsData';
import * as langsData from './modules/module.langsData';
import * as yamlCfg from './modules/module.cfg-loader';
import * as yargs from './modules/module.app-args';
import epsFilter from './modules/module.eps-filter';
import Merger from './modules/module.merger';
// new-cfg paths
const cfg = yamlCfg.loadCfg();
let token = yamlCfg.loadCRToken();
let cmsToken = {};
let cmsToken: {
cms?: Record<string, string>
} = {};
// args
const appYargs = new yargs(cfg.cli, langsData, true);
const argv = appYargs.appArgv();
const argv = yargs.appArgv(cfg.cli)
argv.appstore = {};
// load req
const { domain, api } = require('./crunchy/modules/module.api-urls');
const reqModule = require('./crunchy/modules/module.req');
const req = new reqModule.Req(domain, argv, true);
import { domain, api } from './modules/module.api-urls';
import * as reqModule from './modules/module.req';
import { CrunchySearch, ItemItem, ItemType } from './@types/crunchySearch';
import { CrunchyEpisodeList } from './@types/crunchyEpisodeList';
import { CrunchyEpMeta, ParseItem } from './@types/crunchyTypes';
const req = new reqModule.Req(domain, argv);
// select
(async () => {
@ -84,7 +88,7 @@ const req = new reqModule.Req(domain, argv, true);
async function getFonts(){
console.log('[INFO] Downloading fonts...');
for(const f of Object.keys(fontsData.fonts)){
const fontFile = fontsData.fonts[f];
const fontFile = fontsData.fonts[f as fontsData.AvailableFonts];
const fontLoc = path.join(cfg.dir.fonts, fontFile);
if(fs.existsSync(fontLoc) && fs.statSync(fontLoc).size != 0){
console.log(`[INFO] ${f} (${fontFile}) already downloaded!`);
@ -99,8 +103,8 @@ async function getFonts(){
}
catch(e){}
const fontUrl = fontsData.root + fontFile;
const getFont = await req.getData(fontUrl, { useProxy: true, binary: true });
if(getFont.ok){
const getFont = await req.getData<Buffer>(fontUrl, { binary: true });
if(getFont.ok && getFont.res){
fs.writeFileSync(fontLoc, getFont.res.body);
console.log(`[INFO] Downloaded: ${f} (${fontFile})`);
}
@ -114,22 +118,21 @@ async function getFonts(){
// auth method
async function doAuth(){
const iLogin = argv.user ? argv.user : await shlp.question('[Q] LOGIN/EMAIL');
const iPsswd = argv.pass ? argv.pass : await shlp.question('[Q] PASSWORD ');
const iLogin = await shlp.question('[Q] LOGIN/EMAIL');
const iPsswd = await shlp.question('[Q] PASSWORD ');
const authData = new URLSearchParams({
'username': iLogin,
'password': iPsswd,
'grant_type': 'password',
'scope': 'offline_access'
}).toString();
const authReqOpts = {
const authReqOpts: reqModule.Params = {
method: 'POST',
headers: api.beta_authHeaderMob,
body: authData,
useProxy: true
body: authData
};
const authReq = await req.getData(api.beta_auth, authReqOpts);
if(!authReq.ok){
if(!authReq.ok || !authReq.res){
console.log('[ERROR] Authentication failed!');
return;
}
@ -152,7 +155,7 @@ async function getProfile(){
useProxy: true
};
const profileReq = await req.getData(api.beta_profile, profileReqOptions);
if(!profileReq.ok){
if(!profileReq.ok || !profileReq.res){
console.log('[ERROR] Get profile failed!');
return;
}
@ -166,14 +169,13 @@ async function doAnonymousAuth(){
'grant_type': 'client_id',
'scope': 'offline_access',
}).toString();
const authReqOpts = {
const authReqOpts: reqModule.Params = {
method: 'POST',
headers: api.beta_authHeaderMob,
body: authData,
useProxy: true
body: authData
};
const authReq = await req.getData(api.beta_auth, authReqOpts);
if(!authReq.ok){
if(!authReq.ok || !authReq.res){
console.log('[ERROR] Authentication failed!');
return;
}
@ -196,14 +198,13 @@ async function refreshToken(){
'grant_type': 'refresh_token',
'scope': 'offline_access'
}).toString();
const authReqOpts = {
const authReqOpts: reqModule.Params = {
method: 'POST',
headers: api.beta_authHeaderMob,
body: authData,
useProxy: true
body: authData
};
const authReq = await req.getData(api.beta_auth, authReqOpts);
if(!authReq.ok){
if(!authReq.ok || !authReq.res){
console.log('[ERROR] Authentication failed!');
return;
}
@ -232,12 +233,12 @@ async function getCMStoken(){
useProxy: true
};
const cmsTokenReq = await req.getData(api.beta_cmsToken, cmsTokenReqOpts);
if(!cmsTokenReq.ok){
if(!cmsTokenReq.ok || !cmsTokenReq.res){
console.log('[ERROR] Authentication CMS token failed!');
return;
}
cmsToken = JSON.parse(cmsTokenReq.res.body);
console.log('[INFO] Your Country: %s\n', cmsToken.cms.bucket.split('/')[1]);
console.log('[INFO] Your Country: %s\n', cmsToken.cms?.bucket.split('/')[1]);
}
async function getCmsData(){
@ -257,8 +258,8 @@ async function getCmsData(){
'Key-Pair-Id': cmsToken.cms.key_pair_id,
}),
].join('');
const indexReq = await req.getData(indexReqOpts, { useProxy: true });
if(!indexReq.ok){
const indexReq = await req.getData(indexReqOpts);
if(!indexReq.ok || ! indexReq.res){
console.log('[ERROR] Get CMS index FAILED!');
return;
}
@ -277,18 +278,18 @@ async function doSearch(){
useProxy: true
};
const searchParams = new URLSearchParams({
q: argv.search,
n: 5,
start: argv.page ? (parseInt(argv.page)-1)*5 : 0,
q: argv.search as string,
n: "5",
start: argv.page ? `${(argv.page-1)*5}` : "0",
type: argv['search-type'],
locale: argv['search-locale'],
}).toString();
let searchReq = await req.getData(`${api.beta_search}?${searchParams}`, searchReqOpts);
if(!searchReq.ok){
if(!searchReq.ok || ! searchReq.res){
console.log('[ERROR] Search FAILED!');
return;
}
let searchResults = JSON.parse(searchReq.res.body);
let searchResults = JSON.parse(searchReq.res.body) as CrunchySearch;
if(searchResults.total < 1){
console.log('[INFO] Nothing Found!');
return;
@ -300,9 +301,9 @@ async function doSearch(){
'episode': 'Found episodes'
};
for(let search_item of searchResults.items){
console.log('[INFO] %s:', searchTypesInfo[search_item.type]);
console.log('[INFO] %s:', searchTypesInfo[search_item.type as keyof typeof searchTypesInfo]);
// calculate pages
let itemPad = parseInt(new URL(search_item.__href__, domain.api_beta).searchParams.get('start'));
let itemPad = parseInt(new URL(search_item.__href__, domain.api_beta).searchParams.get('start') || '');
let pageCur = itemPad > 0 ? Math.ceil(itemPad/5) + 1 : 1;
let pageMax = Math.ceil(search_item.total/5);
// pages per category
@ -322,16 +323,17 @@ async function doSearch(){
}
}
async function parseObject(item, pad, getSeries, getMovieListing){
async function parseObject(item: ParseItem, pad?: number, getSeries?: boolean, getMovieListing?: boolean){
if(argv.debug){
console.log(item);
}
pad = typeof pad == 'number' ? pad : 2;
getSeries = typeof getSeries == 'boolean' ? getSeries : true;
getMovieListing = typeof getMovieListing == 'boolean' ? getMovieListing : true;
item.isSelected = typeof item.isSelected == 'boolean' ? item.isSelected : false;
pad = pad || 2;
getSeries = getSeries === undefined ? true : getSeries;
getMovieListing = getMovieListing === undefined ? true : getMovieListing;
item.isSelected = item.isSelected === undefined ? false : item.isSelected;
if(!item.type){
item.type = item.__class__;
console.log('[INFO] Unable to parse type for %s. Defaulted to %s', item.id, ItemType.Episode)
item.type = ItemType.Episode;
}
const oTypes = {
'series': 'Z', // SRZ
@ -396,7 +398,7 @@ async function parseObject(item, pad, getSeries, getMovieListing){
const showObjectBooleans = oBooleans.length > 0 && !iMetadata.hide_metadata ? true : false;
// make obj ids
let objects_ids = [];
objects_ids.push(oTypes[item.type] + ':' + item.id);
objects_ids.push(oTypes[item.type as keyof typeof oTypes] + ':' + item.id);
if(item.seq_id){
objects_ids.unshift(item.seq_id);
}
@ -453,10 +455,10 @@ async function parseObject(item, pad, getSeries, getMovieListing){
}
}
async function getSeriesById(pad, hideSeriesTitle){
async function getSeriesById(pad?: number, hideSeriesTitle?: boolean){
// parse
pad = typeof pad == 'number' ? pad : 0;
hideSeriesTitle = typeof hideSeriesTitle == 'boolean' ? hideSeriesTitle : false;
pad = pad || 0;
hideSeriesTitle = hideSeriesTitle !== undefined ? hideSeriesTitle : false;
// check token
if(!cmsToken.cms){
console.log('[ERROR] Authentication required!');
@ -480,7 +482,7 @@ async function getSeriesById(pad, hideSeriesTitle){
cmsToken.cms.bucket,
'/seasons?',
new URLSearchParams({
'series_id': argv.series,
'series_id': argv.series as string,
'Policy': cmsToken.cms.policy,
'Signature': cmsToken.cms.signature,
'Key-Pair-Id': cmsToken.cms.key_pair_id,
@ -488,8 +490,8 @@ async function getSeriesById(pad, hideSeriesTitle){
].join('');
// reqs
if(!hideSeriesTitle){
const seriesReq = await req.getData(seriesReqOpts, {useProxy: true});
if(!seriesReq.ok){
const seriesReq = await req.getData(seriesReqOpts);
if(!seriesReq.ok || !seriesReq.res){
console.log('[ERROR] Series Request FAILED!');
return;
}
@ -497,8 +499,8 @@ async function getSeriesById(pad, hideSeriesTitle){
await parseObject(seriesData, pad, false);
}
// seasons list
const seriesSeasonListReq = await req.getData(seriesSeasonListReqOpts, {useProxy: true});
if(!seriesSeasonListReq.ok){
const seriesSeasonListReq = await req.getData(seriesSeasonListReqOpts);
if(!seriesSeasonListReq.ok || !seriesSeasonListReq.res){
console.log('[ERROR] Series Request FAILED!');
return;
}
@ -513,8 +515,8 @@ async function getSeriesById(pad, hideSeriesTitle){
}
}
async function getMovieListingById(pad){
pad = typeof pad == 'number' ? pad : 2;
async function getMovieListingById(pad?: number){
pad = pad || 2;
if(!cmsToken.cms){
console.log('[ERROR] Authentication required!');
return;
@ -524,14 +526,14 @@ async function getMovieListingById(pad){
cmsToken.cms.bucket,
'/movies?',
new URLSearchParams({
'movie_listing_id': argv['movie-listing'],
'movie_listing_id': argv['movie-listing'] as string,
'Policy': cmsToken.cms.policy,
'Signature': cmsToken.cms.signature,
'Key-Pair-Id': cmsToken.cms.key_pair_id,
}),
].join('');
const movieListingReq = await req.getData(movieListingReqOpts, {useProxy: true});
if(!movieListingReq.ok){
const movieListingReq = await req.getData(movieListingReqOpts);
if(!movieListingReq.ok || !movieListingReq.res){
console.log('[ERROR] Movie Listing Request FAILED!');
return;
}
@ -558,11 +560,11 @@ async function getNewlyAdded(){
};
const newlyAddedParams = new URLSearchParams({
sort_by: 'newly_added',
n: 25,
start: argv.page ? (parseInt(argv.page)-1)*25 : 0,
n: "25",
start: (argv.page ? (argv.page-1)*25 : 0).toString(),
}).toString();
let newlyAddedReq = await req.getData(`${api.beta_browse}?${newlyAddedParams}`, newlyAddedReqOpts);
if(!newlyAddedReq.ok){
if(!newlyAddedReq.ok || !newlyAddedReq.res){
console.log('[ERROR] Get newly added FAILED!');
return;
}
@ -572,7 +574,7 @@ async function getNewlyAdded(){
await parseObject(i, 2);
}
// calculate pages
let itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.api_beta).searchParams.get('start'));
let itemPad = parseInt(new URL(newlyAddedResults.__href__, domain.api_beta).searchParams.get('start') as string);
let pageCur = itemPad > 0 ? Math.ceil(itemPad/5) + 1 : 1;
let pageMax = Math.ceil(newlyAddedResults.total/5);
console.log(` [INFO] Total results: ${newlyAddedResults.total} (Page: ${pageCur}/${pageMax})`);
@ -596,8 +598,8 @@ async function getSeasonById(){
'Key-Pair-Id': cmsToken.cms.key_pair_id,
}),
].join('');
const showInfoReq = await req.getData(showInfoReqOpts, {useProxy: true});
if(!showInfoReq.ok){
const showInfoReq = await req.getData(showInfoReqOpts);
if(!showInfoReq.ok || !showInfoReq.res){
console.log('[ERROR] Show Request FAILED!');
return;
}
@ -608,20 +610,23 @@ async function getSeasonById(){
cmsToken.cms.bucket,
'/episodes?',
new URLSearchParams({
'season_id': argv.season,
'season_id': argv.season as string,
'Policy': cmsToken.cms.policy,
'Signature': cmsToken.cms.signature,
'Key-Pair-Id': cmsToken.cms.key_pair_id,
}),
].join('');
const reqEpsList = await req.getData(reqEpsListOpts, {useProxy: true});
if(!reqEpsList.ok){
const reqEpsList = await req.getData(reqEpsListOpts);
if(!reqEpsList.ok || !reqEpsList.res){
console.log('[ERROR] Episode List Request FAILED!');
return;
}
let episodeList = JSON.parse(reqEpsList.res.body);
let episodeList = JSON.parse(reqEpsList.res.body) as CrunchyEpisodeList;
const epNumList = { ep: [], sp: 0 };
const epNumList: {
ep: number[],
sp: number
} = { ep: [], sp: 0 };
const epNumLen = epsFilter.epNumLen;
if(episodeList.total < 1){
@ -631,7 +636,7 @@ async function getSeasonById(){
const doEpsFilter = new epsFilter.doFilter();
const selEps = doEpsFilter.checkFilter(argv.episode);
const selectedMedia = [];
const selectedMedia: CrunchyEpMeta[] = [];
episodeList.items.forEach((item) => {
item.hide_season_title = true;
@ -644,7 +649,7 @@ async function getSeasonById(){
item.season_title = 'NO_TITLE';
}
// set data
const epMeta = {
const epMeta: CrunchyEpMeta = {
mediaId: item.id,
seasonTitle: item.season_title,
episodeNumber: item.episode,
@ -685,7 +690,7 @@ async function getSeasonById(){
}
if(selectedMedia.length > 1){
argv.appstore.isBatch = true;
(argv.appstore as Record<string, unknown>).isBatch = true;
}
console.log();

View file

@ -1,285 +0,0 @@
const path = require('path');
const fs = require('fs-extra');
const shlp = require('sei-helper');
const got = require('got');
const cookieFile = require('./module.cookieFile');
const yamlCfg = require('./module.cfg-loader');
const curlReq = require('./module.curl-req');
// set usable cookies
const usefulCookies = {
auth: [
'etp_rt',
'c_visitor',
],
sess: [
'session_id',
],
};
// req
const Req = class {
constructor(domain, argv, is_beta){
// settings and cookies
this.is_beta = Boolean(is_beta);
this.loadSessTxt = this.is_beta ? false : true;
// main cfg
this.domain = domain;
this.argv = argv;
// session cfg
this.sessCfg = yamlCfg.sessCfgFile,
this.session = this.is_beta ? {} : yamlCfg.loadCRSession();
this.cfgDir = yamlCfg.cfgFolder;
this.curl = false;
}
async getData (durl, params) {
params = params || {};
// options
let options = {
method: params.method ? params.method : 'GET',
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:90.0) Gecko/20100101 Firefox/90.0',
},
};
// additional params
if(params.headers){
options.headers = {...options.headers, ...params.headers};
}
if(options.method == 'POST'){
options.headers['Content-Type'] = 'application/x-www-form-urlencoded';
}
if(params.body){
options.body = params.body;
}
if(params.binary == true){
options.responseType = 'buffer';
}
if(typeof params.followRedirect == 'boolean'){
options.followRedirect = params.followRedirect;
}
// check if cookies.txt exists
const sessTxtFile = path.join(this.cfgDir, 'cookies.txt');
if(!this.is_beta && this.loadSessTxt && fs.existsSync(sessTxtFile)){
const cookiesTxtName = path.basename(sessTxtFile);
try{
// console.log(`[INFO] Loading custom ${cookiesTxtName} file...`);
const netcookie = fs.readFileSync(sessTxtFile, 'utf8');
fs.unlinkSync(sessTxtFile);
this.setNewCookie('', true, netcookie);
}
catch(e){
console.log(`[ERROR] Cannot load ${cookiesTxtName} file!`);
}
}
this.loadSessTxt = false;
// proxy
if(params.useProxy && this.argv.proxy && this.argv.curl){
try{
options.curlProxy = buildProxy(this.argv.proxy);
options.curlProxyAuth = this.argv['proxy-auth'];
}
catch(e){
console.log(`[WARN] Not valid proxy URL${e.input?' ('+e.input+')':''}!`);
console.log('[WARN] Skipping...\n');
this.argv.proxy = false;
}
}
// if auth
let cookie = [];
const loc = new URL(durl);
if(!this.is_beta && Object.values(this.domain).includes(loc.origin)){
for(let uCookie of usefulCookies.auth){
const checkedCookie = this.checkCookieVal(this.session[uCookie]);
if(checkedCookie){
cookie.push(uCookie);
}
}
for(let uCookie of usefulCookies.sess){
if(this.checkSessId(this.session[uCookie]) && !this.argv.nosess){
cookie.push(uCookie);
}
}
if(!params.skipCookies){
cookie.push('c_locale');
options.headers.Cookie = shlp.cookie.make({
...{ c_locale : { value: 'enUS' } },
...this.session,
}, cookie);
}
}
// avoid cloudflare protection
if(loc.origin == this.domain.www){
options.minVersion = 'TLSv1.3';
options.maxVersion = 'TLSv1.3';
options.http2 = true;
}
// debug
options.hooks = {
beforeRequest: [
(options) => {
if(this.argv.debug){
console.log('[DEBUG] GOT OPTIONS:');
console.log(options);
}
}
]
};
if(this.argv.debug){
options.curlDebug = true;
}
// try do request
try {
let res;
if(this.curl && this.argv.curl && Object.values(this.domain).includes(loc.origin)){
res = await curlReq(this.curl, durl.toString(), options, this.cfgDir);
}
else{
res = await got(durl.toString(), options);
}
if(!this.is_beta && !params.skipCookies && res && res.headers && res.headers['set-cookie']){
this.setNewCookie(res.headers['set-cookie'], false);
for(let uCookie of usefulCookies.sess){
if(this.session[uCookie] && this.argv.nosess){
this.argv.nosess = false;
}
}
}
return {
ok: true,
res,
};
}
catch(error){
if(error.response && error.response.statusCode && error.response.statusMessage){
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
}
else{
console.log(`[ERROR] ${error.name}: ${error.code || error.message}`);
}
if(error.response && !error.res){
error.res = error.response;
const docTitle = error.res.body.match(/<title>(.*)<\/title>/);
if(error.res.body && docTitle){
console.log('[ERROR]', docTitle[1]);
}
}
if(error.res && error.res.body && error.response.statusCode
&& error.response.statusCode != 404 && error.response.statusCode != 403){
console.log('[ERROR] Body:', error.res.body);
}
return {
ok: false,
error,
};
}
}
setNewCookie(setCookie, isAuth, fileData){
let cookieUpdated = [], lastExp = 0;
setCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie);
for(let cookieName of Object.keys(setCookie)){
if(setCookie[cookieName] && setCookie[cookieName].value && setCookie[cookieName].value == 'deleted'){
delete setCookie[cookieName];
}
}
for(let uCookie of usefulCookies.auth){
const cookieForceExp = 60*60*24*7;
const cookieExpCur = this.session[uCookie] ? this.session[uCookie] : { expires: 0 };
const cookieExp = new Date(cookieExpCur.expires).getTime() - cookieForceExp;
if(cookieExp > lastExp){
lastExp = cookieExp;
}
}
for(let uCookie of usefulCookies.auth){
if(!setCookie[uCookie]){
continue;
}
if(isAuth || setCookie[uCookie] && Date.now() > lastExp){
this.session[uCookie] = setCookie[uCookie];
cookieUpdated.push(uCookie);
}
}
for(let uCookie of usefulCookies.sess){
if(!setCookie[uCookie]){
continue;
}
if(
isAuth
|| this.argv.nosess && setCookie[uCookie]
|| setCookie[uCookie] && !this.checkSessId(this.session[uCookie])
){
const sessionExp = 60*60;
this.session[uCookie] = setCookie[uCookie];
this.session[uCookie].expires = new Date(Date.now() + sessionExp*1000);
this.session[uCookie]['Max-Age'] = sessionExp.toString();
cookieUpdated.push(uCookie);
}
}
if(cookieUpdated.length > 0){
if(this.argv.debug){
console.log('[SAVING FILE]',`${this.sessCfg}.yml`);
}
yamlCfg.saveCRSession(this.session);
console.log(`[INFO] Cookies were updated! (${cookieUpdated.join(', ')})\n`);
}
}
checkCookieVal(chcookie){
return chcookie
&& chcookie.toString() == '[object Object]'
&& typeof chcookie.value == 'string'
? true : false;
}
checkSessId(session_id){
if(session_id && typeof session_id.expires == 'string'){
session_id.expires = new Date(session_id.expires);
}
return session_id
&& session_id.toString() == '[object Object]'
&& typeof session_id.expires == 'object'
&& Date.now() < new Date(session_id.expires).getTime()
&& typeof session_id.value == 'string'
? true : false;
}
uuidv4(){
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
};
function buildProxy(proxyBaseUrl, proxyAuth){
if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){
proxyBaseUrl = 'http://' + proxyBaseUrl;
}
let 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;
}
module.exports = {
buildProxy,
usefulCookies,
Req,
};

View file

@ -55,7 +55,7 @@ let title = '',
stDlPath: Subtitle[] = [];
// main
(async () => {
export default (async () => {
// load binaries
cfg.bin = await yamlCfg.loadBinCfg();
// select mode
@ -65,13 +65,13 @@ let title = '',
else if(argv.search){
searchShow();
}
else if(argv.s && argv.s > 0){
else if(argv.s && !isNaN(parseInt(argv.s)) && parseInt(argv.s) > 0){
getShow();
}
else{
appYargs.showHelp();
}
})();
});
// auth
async function auth(){
@ -157,7 +157,7 @@ async function getShow(){
sort_direction: string,
title_id: number,
language?: string
} = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: argv.s as number };
} = { limit: -1, sort: 'order', sort_direction: 'ASC', title_id: parseInt(argv.s as string) };
if(argv.alt){ qs.language = 'English'; }
const episodesData = await getData({
baseUrl: api_host,

View file

@ -1 +1,17 @@
import { appArgv } from "./modules/module.app-args";
import { appArgv } from "./modules/module.app-args";
import * as yamlCfg from './modules/module.cfg-loader';
import funimation from './funi'
(async () => {
const cfg = yamlCfg.loadCfg();
const argv = appArgv(cfg.cli);
if (argv.service === 'funi') {
await funimation()
} else if (argv.service === 'crunchy') {
}
})()

View file

@ -1,3 +1,5 @@
import { Headers } from "got/dist/source";
// api domains
const domain = {
www: 'https://www.crunchyroll.com',
@ -28,8 +30,8 @@ export type APIType = {
beta_search: string
beta_browse: string
beta_cms: string,
beta_authHeader: HeadersInit,
beta_authHeaderMob: HeadersInit
beta_authHeader: Headers,
beta_authHeaderMob: Headers
}
// api urls

View file

@ -119,9 +119,9 @@ const appArgv = (cfg: {
default: parseDefault<number>('videoLayer', 7),
type: 'number'
})
.option('server', {
.option('x', {
group: groups.dl,
alias: 'x',
alias: 'server',
describe: 'Select server',
choices: [1, 2, 3, 4],
default: parseDefault<number>('nServer', 1),
@ -260,7 +260,14 @@ const appArgv = (cfg: {
describe: 'Show this help',
type: 'boolean'
})
.option('service', {
group: groups.util,
describe: 'Set the service to use',
choices: ['funi', 'chrunchy'],
demandOption: true
})
.parseSync();
return argv;
}
const showHelp = yargs.showHelp;

View file

@ -192,4 +192,17 @@ const saveFuniToken = (data: {
}
};
export { loadBinCfg, loadCfg, loadFuniToken, saveFuniToken, saveCRSession, saveCRToken, loadCRToken, loadCRSession };
const cfgDir = path.join(workingDir, 'config');
export {
loadBinCfg,
loadCfg,
loadFuniToken,
saveFuniToken,
saveCRSession,
saveCRToken,
loadCRToken,
loadCRSession,
sessCfgFile,
cfgDir
};

View file

@ -23,4 +23,4 @@ const parse = (data: string) => {
return res;
};
module.exports = parse;
export default parse;

View file

@ -1,14 +1,15 @@
// build-in
import child_process from 'child_process';
import fs from 'fs-extra';
import { Headers } from 'got';
import path from 'path';
export type CurlOptions = {
headers?: Record<string, string>,
headers?: Headers,
curlProxy?: boolean,
curlProxyAuth?: string,
minVersion?: string,
http2?: string,
http2?: boolean,
body?: unknown,
curlDebug?: boolean
} | undefined;
@ -158,4 +159,4 @@ function uuidv4() {
});
}
module.exports = curlReq;
export default curlReq;

View file

@ -89,5 +89,7 @@ function fontMime(fontFile: string){
return 'application/octet-stream';
}
export type AvailableFonts = keyof typeof fonts;
// output
export { root, fonts, assFonts, fontMime };

View file

@ -29,6 +29,7 @@ export type Options = {
dinstid?: boolean|string,
debug?: boolean
}
// TODO convert to class
const getData = async <T = string>(options: Options) => {
const regionHeaders = {};

261
modules/module.req.ts Normal file
View file

@ -0,0 +1,261 @@
import path from 'path';
import fs from 'fs-extra';
import shlp from 'sei-helper';
import got, { Headers, Method, Options, ReadError, Response } from 'got';
import cookieFile from './module.cookieFile';
import * as yamlCfg from './module.cfg-loader';
import curlReq from './module.curl-req';
export type Params = {
method?: Method,
headers?: Headers,
body?: string | Buffer,
binary?: boolean,
followRedirect?: boolean
}
// set usable cookies
const usefulCookies = {
auth: [
'etp_rt',
'c_visitor',
],
sess: [
'session_id',
],
};
// req
class Req {
private sessCfg = yamlCfg.sessCfgFile;
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 argv: Record<string, unknown>) {}
async getData<T = string> (durl: string, params?: Params) {
params = params || {};
// options
let options: Options & {
minVersion?: string,
maxVersion?: string
curlDebug?: boolean
} = {
method: params.method ? params.method : 'GET',
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:90.0) Gecko/20100101 Firefox/90.0',
},
};
// additional params
if(params.headers){
options.headers = {...options.headers, ...params.headers};
}
if(options.method == 'POST'){
(options.headers as Headers)['Content-Type'] = 'application/x-www-form-urlencoded';
}
if(params.body){
options.body = params.body;
}
if(params.binary == true){
options.responseType = 'buffer';
}
if(typeof params.followRedirect == 'boolean'){
options.followRedirect = params.followRedirect;
}
// Removed Proxy support since it was only partialy supported
/*if(params.useProxy && this.argv.proxy && this.argv.curl){
try{
options.curlProxy = buildProxy(this.argv.proxy);
options.curlProxyAuth = this.argv['proxy-auth'];
}
catch(e){
console.log(`[WARN] Not valid proxy URL${e.input?' ('+e.input+')':''}!`);
console.log('[WARN] Skipping...\n');
this.argv.proxy = false;
}
}*/
// if auth
let cookie = [];
const loc = new URL(durl);
// avoid cloudflare protection
if(loc.origin == this.domain.www){
options.minVersion = 'TLSv1.3';
options.maxVersion = 'TLSv1.3';
options.http2 = true;
}
// debug
options.hooks = {
beforeRequest: [
(options) => {
if(this.argv.debug){
console.log('[DEBUG] GOT OPTIONS:');
console.log(options);
}
}
]
};
if(this.argv.debug){
options.curlDebug = true;
}
// try do request
try {
let res: Response<T>;
if(this.curl && this.argv.curl && Object.values(this.domain).includes(loc.origin)){
res = await curlReq(typeof this.curl === 'boolean' ? '' : this.curl, durl.toString(), options, this.cfgDir) as unknown as Response<T>;
}
else{
res = await got(durl.toString(), options) as unknown as Response<T>;
}
return {
ok: true,
res
};
}
catch(_error){
const error = _error as {
name: string
} & ReadError & {
res: Response<unknown>
}
if(error.response && error.response.statusCode && error.response.statusMessage){
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
}
else{
console.log(`[ERROR] ${error.name}: ${error.code || error.message}`);
}
if(error.response && !error.res){
error.res = error.response;
const docTitle = (error.res.body as string).match(/<title>(.*)<\/title>/);
if(error.res.body && docTitle){
console.log('[ERROR]', docTitle[1]);
}
}
if(error.res && error.res.body && error.response.statusCode
&& error.response.statusCode != 404 && error.response.statusCode != 403){
console.log('[ERROR] Body:', error.res.body);
}
return {
ok: false,
error,
};
}
}
setNewCookie(setCookie: Record<string, string>, isAuth: boolean, fileData?: string){
let cookieUpdated = [], lastExp = 0;
console.trace('Type of setCookie:', typeof setCookie, setCookie)
const parsedCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie);
for(let cookieName of Object.keys(parsedCookie)){
if(parsedCookie[cookieName] && parsedCookie[cookieName].value && parsedCookie[cookieName].value == 'deleted'){
delete parsedCookie[cookieName];
}
}
for(let uCookie of usefulCookies.auth){
const cookieForceExp = 60*60*24*7;
const cookieExpCur = this.session[uCookie] ? this.session[uCookie] : { expires: 0 };
const cookieExp = new Date(cookieExpCur.expires).getTime() - cookieForceExp;
if(cookieExp > lastExp){
lastExp = cookieExp;
}
}
for(let uCookie of usefulCookies.auth){
if(!parsedCookie[uCookie]){
continue;
}
if(isAuth || parsedCookie[uCookie] && Date.now() > lastExp){
this.session[uCookie] = parsedCookie[uCookie];
cookieUpdated.push(uCookie);
}
}
for(let uCookie of usefulCookies.sess){
if(!parsedCookie[uCookie]){
continue;
}
if(
isAuth
|| this.argv.nosess && parsedCookie[uCookie]
|| parsedCookie[uCookie] && !this.checkSessId(this.session[uCookie])
){
const sessionExp = 60*60;
this.session[uCookie] = parsedCookie[uCookie];
this.session[uCookie].expires = new Date(Date.now() + sessionExp*1000);
this.session[uCookie]['Max-Age'] = sessionExp.toString();
cookieUpdated.push(uCookie);
}
}
if(cookieUpdated.length > 0){
if(this.argv.debug){
console.log('[SAVING FILE]',`${this.sessCfg}.yml`);
}
yamlCfg.saveCRSession(this.session);
console.log(`[INFO] Cookies were updated! (${cookieUpdated.join(', ')})\n`);
}
}
checkCookieVal(chcookie: Record<string, string>){
return chcookie
&& chcookie.toString() == '[object Object]'
&& typeof chcookie.value == 'string'
? true : false;
}
checkSessId(session_id: Record<string, unknown>){
if(session_id && typeof session_id.expires == 'string'){
session_id.expires = new Date(session_id.expires);
}
return session_id
&& session_id.toString() == '[object Object]'
&& typeof session_id.expires == 'object'
&& Date.now() < new Date(session_id.expires as any).getTime()
&& typeof session_id.value == 'string'
? true : false;
}
uuidv4(){
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
};
function buildProxy(proxyBaseUrl: string, proxyAuth: string){
if(!proxyBaseUrl.match(/^(https?|socks4|socks5):/)){
proxyBaseUrl = 'http://' + proxyBaseUrl;
}
let 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;
}
export {
buildProxy,
usefulCookies,
Req,
};