Update + Update Version + Eslint

This commit is contained in:
Izuco 2022-02-20 19:56:45 +01:00
parent a307b9b1ed
commit a7c7451b0d
54 changed files with 8581 additions and 9106 deletions

View file

@ -1,119 +1,119 @@
export interface CrunchyEpisodeList {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: unknown;
__actions__: unknown;
total: number;
items: Item[];
}
export interface Item {
__class__: Class;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: unknown;
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',
}
export interface CrunchyEpisodeList {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: unknown;
__actions__: unknown;
total: number;
items: Item[];
}
export interface Item {
__class__: Class;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: unknown;
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',
}

View file

@ -1,165 +1,165 @@
// Generated by https://quicktype.io
export interface CrunchySearch {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: CrunchySearchLinks;
__actions__: unknown;
total: number;
items: CrunchySearchItem[];
}
export interface CrunchySearchLinks {
continuation?: Continuation;
}
export interface Continuation {
href: string;
}
export interface CrunchySearchItem {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: CrunchySearchLinks;
__actions__: unknown;
type: string;
total: number;
items: ItemItem[];
}
export interface ItemItem {
__actions__: unknown;
__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[];
// Generated by https://quicktype.io
export interface CrunchySearch {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: CrunchySearchLinks;
__actions__: unknown;
total: number;
items: CrunchySearchItem[];
}
export interface CrunchySearchLinks {
continuation?: Continuation;
}
export interface Continuation {
href: string;
}
export interface CrunchySearchItem {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: CrunchySearchLinks;
__actions__: unknown;
type: string;
total: number;
items: ItemItem[];
}
export interface ItemItem {
__actions__: unknown;
__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[];
}

View file

@ -1,128 +1,128 @@
import { HLSCallback } from 'hls-download';
import { sxItem } from '../crunchy';
import { LanguageItem } from '../modules/module.langsData';
import { DownloadInfo } from './messageHandler';
export type CrunchyDownloadOptions = {
hslang: string,
kstream: number,
novids?: boolean,
x: number,
q: number,
fileName: string,
numbers: number,
partsize: number,
callbackMaker?: (data: DownloadInfo) => HLSCallback,
timeout: number,
fsRetryTime: number,
dlsubs: string[],
skipsubs: boolean,
mp4: boolean
}
export type CurnchyMultiDownload = {
dubLang: string[],
all?: boolean,
but?: boolean,
e?: string
}
export type CrunchyMuxOptions = {
output: string,
skipSubMux?: boolean
novids?: boolean,
mp4: boolean,
forceMuxer?: 'ffmpeg'|'mkvmerge',
nocleanup?: boolean
}
export type CrunchyEpMeta = {
data: {
mediaId: string,
lang?: LanguageItem,
playback?: string
}[],
seasonTitle: string,
episodeNumber: string,
episodeTitle: string,
seasonID: string,
season: number,
showID: string,
e: string,
image: string
}
export type DownloadedMedia = {
type: 'Video',
lang: LanguageItem,
path: string
} | ({
type: 'Subtitle'
} & sxItem )
export type ParseItem = {
__class__?: string;
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
}
export interface SeriesSearch {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Actions;
__actions__: Actions;
total: number;
items: SeriesSearchItem[];
}
export interface SeriesSearchItem {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: string[];
id: string;
channel_id: string;
title: string;
slug_title: string;
series_id: string;
season_number: number;
is_complete: boolean;
description: string;
keywords: any[];
season_tags: string[];
images: Actions;
is_mature: boolean;
mature_blocked: boolean;
is_subbed: boolean;
is_dubbed: boolean;
is_simulcast: boolean;
seo_title: string;
seo_description: string;
availability_notes: string;
}
export interface Links {
'season/channel': Season;
'season/episodes': Season;
'season/series': Season;
}
export interface Season {
href: string;
}
import { HLSCallback } from 'hls-download';
import { sxItem } from '../crunchy';
import { LanguageItem } from '../modules/module.langsData';
import { DownloadInfo } from './messageHandler';
export type CrunchyDownloadOptions = {
hslang: string,
kstream: number,
novids?: boolean,
x: number,
q: number,
fileName: string,
numbers: number,
partsize: number,
callbackMaker?: (data: DownloadInfo) => HLSCallback,
timeout: number,
fsRetryTime: number,
dlsubs: string[],
skipsubs: boolean,
mp4: boolean
}
export type CurnchyMultiDownload = {
dubLang: string[],
all?: boolean,
but?: boolean,
e?: string
}
export type CrunchyMuxOptions = {
output: string,
skipSubMux?: boolean
novids?: boolean,
mp4: boolean,
forceMuxer?: 'ffmpeg'|'mkvmerge',
nocleanup?: boolean
}
export type CrunchyEpMeta = {
data: {
mediaId: string,
lang?: LanguageItem,
playback?: string
}[],
seasonTitle: string,
episodeNumber: string,
episodeTitle: string,
seasonID: string,
season: number,
showID: string,
e: string,
image: string
}
export type DownloadedMedia = {
type: 'Video',
lang: LanguageItem,
path: string
} | ({
type: 'Subtitle'
} & sxItem )
export type ParseItem = {
__class__?: string;
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
}
export interface SeriesSearch {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Actions;
__actions__: Actions;
total: number;
items: SeriesSearchItem[];
}
export interface SeriesSearchItem {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: Links;
__actions__: string[];
id: string;
channel_id: string;
title: string;
slug_title: string;
series_id: string;
season_number: number;
is_complete: boolean;
description: string;
keywords: any[];
season_tags: string[];
images: Actions;
is_mature: boolean;
mature_blocked: boolean;
is_subbed: boolean;
is_dubbed: boolean;
is_simulcast: boolean;
seo_title: string;
seo_description: string;
availability_notes: string;
}
export interface Links {
'season/channel': Season;
'season/episodes': Season;
'season/series': Season;
}
export interface Season {
href: string;
}

View file

@ -1,6 +1,6 @@
import { LanguageItem } from '../modules/module.langsData';
export type DownloadedFile = {
path: string,
lang: LanguageItem
import { LanguageItem } from '../modules/module.langsData';
export type DownloadedFile = {
path: string,
lang: LanguageItem
}

782
@types/episode.d.ts vendored
View file

@ -1,391 +1,391 @@
// Generated by https://quicktype.io
export interface EpisodeData {
id: number;
title: string;
mediaDict: { [key: string]: string };
episodeSlug: string;
starRating: number;
parent: EpisodeDataParent;
number: string;
description: string;
filename: string;
seriesBanner: string;
media: Media[];
externalItemId: string;
contentId: string;
metaItems: MetaItems;
thumb: string;
type: Type;
default: { [key: string]: Default };
published: boolean;
versions: VersionClass[];
mediaCategory: string;
order: number;
seriesVersions: any[];
source: Source;
ids: EpisodeDataIDS;
runtime: string;
siblings: PreviousSeasonEpisode[];
seriesTitle: string;
seriesSlug: string;
next: Next;
previousSeasonEpisode: PreviousSeasonEpisode;
seasonTitle: string;
quality: Quality;
ratings: Array<string[]>;
languages: TitleElement[];
releaseDate: string;
historicalSelections: HistoricalSelections;
userRating: UserRating;
}
export interface Default {
items: DefaultItem[];
}
export interface DefaultItem {
languages: string[];
territories: string[];
version: null;
value: Value[];
devices: any[];
}
export interface Value {
name: MetaType;
value: string;
label: Label;
}
export enum Label {
Rating = 'Rating',
RatingSystem = 'Rating System',
ReleaseDate = 'Release Date',
Synopsis = 'Synopsis',
SynopsisType = 'Synopsis Type',
}
export enum MetaType {
Rating = 'rating',
RatingSystemType = 'RatingSystemType',
ReleaseDate = 'release-date',
Synopsis = 'synopsis',
Synopsistype = 'synopsistype',
VideoRatingType = 'VideoRatingType',
}
export interface HistoricalSelections {
version: string;
language: string;
}
export interface EpisodeDataIDS {
externalShowId: string;
externalSeasonId: string;
externalEpisodeId: string;
}
export enum TitleElement {
Empty = '',
English = 'English',
}
export interface Media {
id: number;
title: string;
experienceType: string;
created: string;
createdBy: string;
itemFieldData: Next;
keyPath: string;
filename: string;
complianceStatus: null;
events: any[];
clients: string[];
qcStatus: null;
qcStatusDate: null;
image: string;
thumb: string;
ext: string;
avails: Avail[];
version: string;
startTimecode: null;
endTimecode: null;
versionId: string;
mediaType: string;
status: string;
languages: LanguageClass[];
territories: any[];
devices: any[];
keyType: string;
purpose: null;
externalItemId: null | string;
proxyId: null;
externalDbId: null;
mediaChildren: MediaChild[];
isDefault: boolean;
parent: MediaChildParent;
filePath: null | string;
mediaInfo: Next;
type: string;
approved: boolean;
mediaKey: string;
itemFields: any[];
source: Source;
fieldData: Next;
sourceId: null | string;
timecodeOverride: null;
seriesTitle: string;
episodeTitle: string;
genre: any[];
txDate: string;
description: string;
synopsis: string;
resolution: null;
restrictedAccess: boolean;
createdById: string;
userIdsWithAccess: any[];
runtime?: number;
language?: TitleElement;
purchased: boolean;
}
export interface Avail {
id: number;
description: string;
endDate: string;
startDate: string;
ids: AvailIDS;
originalAirDate: null;
physicalReleaseDate: null;
preorderDate: null;
language: TitleElement;
territory: string;
territoryCode: string;
license: string;
parentAvail: null;
item: number;
version: string;
applyToLevel: null;
availLevel: string;
availDisplayCode: string;
availStatus: string;
bundleOnly: boolean;
contentOwnerOrganization: string;
currency: null;
price: null;
purchase: string;
priceValue: string;
resolutionFormat: null;
runtimeMilliseconds: null;
seasonOrEpisodeNumber: null;
tmsid: null;
deviceList: string;
tvodSku: null;
}
export interface AvailIDS {
externalSeasonId: string;
externalAsianId: null;
externalShowId: string;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
}
export type Next = Record<string, unknown>
export interface LanguageClass {
code: string;
id: number;
title: TitleElement;
}
export interface MediaChild {
id: number;
title: string;
experienceType: string;
created: string;
createdBy: string;
itemFieldData: Next;
keyPath: null;
filename: string;
complianceStatus: null;
events: any[];
clients: string[];
qcStatus: null;
qcStatusDate: null;
image: string;
ext: string;
avails: any[];
version: string;
startTimecode: null;
endTimecode: null;
versionId: string;
mediaType: string;
status: string;
languages: LanguageClass[];
territories: any[];
devices: any[];
keyType: string;
purpose: null;
externalItemId: string;
proxyId: null;
externalDbId: null;
mediaChildren: any[];
isDefault: boolean;
parent: MediaChildParent;
filePath: string;
mediaInfo: MediaInfo;
type: string;
approved: boolean;
mediaKey: null;
itemFields: any[];
source: Source;
fieldData: Next;
sourceId: null;
timecodeOverride: null;
seriesTitle: string;
episodeTitle: string;
genre: any[];
txDate: string;
description: string;
synopsis: string;
resolution: null | string;
restrictedAccess: boolean;
createdById: string;
userIdsWithAccess: any[];
language: TitleElement;
}
export interface MediaInfo {
imageAspectRatio: null | string;
format: string;
scanMode: null | string;
burnedInSubtitleLanguage: string;
screenAspectRatio: null | string;
subtitleFormat: null | string;
subtitleContent: null | string;
frameHeight: number | null;
frameWidth: number | null;
video: Video;
}
export interface Video {
codecId: null | string;
container: null | string;
encodingRate: number | null;
frameRate: null | string;
height: number | null;
width: number | null;
duration: number | null;
bitRate: number | null;
}
export interface MediaChildParent {
title: string;
type: string;
catalogParent: CatalogParent;
slug: string;
grandparentId: number;
id: number;
}
export interface CatalogParent {
id: number;
title: string;
}
export enum Source {
Dbb = 'dbb',
}
export interface MetaItems {
items: Items;
filters: Filters;
}
export interface Filters {
territory: any[];
language: any[];
}
export interface Items {
'release-date': AnimationProductionStudio;
rating: AnimationProductionStudio;
synopsis: AnimationProductionStudio;
'animation-production-studio': AnimationProductionStudio;
}
export interface AnimationProductionStudio {
items: AnimationProductionStudioItem[];
label: string;
id: number;
slug: string;
}
export interface AnimationProductionStudioItem {
id: number;
metaType: MetaType;
metaTypeId: string;
client: null;
languages: TitleElement;
territories: string;
devices: string;
isDefault: boolean;
value: Value[];
approved: boolean;
version: null;
source: Source;
}
export interface EpisodeDataParent {
seasonId: number;
seasonNumber: string;
title: string;
titleSlug: string;
titleType: string;
titleId: number;
}
export interface PreviousSeasonEpisode {
seasonTitle?: string;
mediaCategory: Type;
thumb: string;
title: string;
image: string;
number: string;
id: number;
version: string[];
order: number;
slug: string;
season?: number;
languages?: TitleElement[];
}
export enum Type {
Episode = 'episode',
Ova = 'ova',
}
export interface Quality {
quality: string;
height: number;
}
export interface UserRating {
overall: number;
ja: number;
eng: number;
}
export interface VersionClass {
compliance_approved: boolean;
title: string;
version_id: string;
is_default: boolean;
runtime: string;
external_id: string;
id: number;
}
// Generated by https://quicktype.io
export interface EpisodeData {
id: number;
title: string;
mediaDict: { [key: string]: string };
episodeSlug: string;
starRating: number;
parent: EpisodeDataParent;
number: string;
description: string;
filename: string;
seriesBanner: string;
media: Media[];
externalItemId: string;
contentId: string;
metaItems: MetaItems;
thumb: string;
type: Type;
default: { [key: string]: Default };
published: boolean;
versions: VersionClass[];
mediaCategory: string;
order: number;
seriesVersions: any[];
source: Source;
ids: EpisodeDataIDS;
runtime: string;
siblings: PreviousSeasonEpisode[];
seriesTitle: string;
seriesSlug: string;
next: Next;
previousSeasonEpisode: PreviousSeasonEpisode;
seasonTitle: string;
quality: Quality;
ratings: Array<string[]>;
languages: TitleElement[];
releaseDate: string;
historicalSelections: HistoricalSelections;
userRating: UserRating;
}
export interface Default {
items: DefaultItem[];
}
export interface DefaultItem {
languages: string[];
territories: string[];
version: null;
value: Value[];
devices: any[];
}
export interface Value {
name: MetaType;
value: string;
label: Label;
}
export enum Label {
Rating = 'Rating',
RatingSystem = 'Rating System',
ReleaseDate = 'Release Date',
Synopsis = 'Synopsis',
SynopsisType = 'Synopsis Type',
}
export enum MetaType {
Rating = 'rating',
RatingSystemType = 'RatingSystemType',
ReleaseDate = 'release-date',
Synopsis = 'synopsis',
Synopsistype = 'synopsistype',
VideoRatingType = 'VideoRatingType',
}
export interface HistoricalSelections {
version: string;
language: string;
}
export interface EpisodeDataIDS {
externalShowId: string;
externalSeasonId: string;
externalEpisodeId: string;
}
export enum TitleElement {
Empty = '',
English = 'English',
}
export interface Media {
id: number;
title: string;
experienceType: string;
created: string;
createdBy: string;
itemFieldData: Next;
keyPath: string;
filename: string;
complianceStatus: null;
events: any[];
clients: string[];
qcStatus: null;
qcStatusDate: null;
image: string;
thumb: string;
ext: string;
avails: Avail[];
version: string;
startTimecode: null;
endTimecode: null;
versionId: string;
mediaType: string;
status: string;
languages: LanguageClass[];
territories: any[];
devices: any[];
keyType: string;
purpose: null;
externalItemId: null | string;
proxyId: null;
externalDbId: null;
mediaChildren: MediaChild[];
isDefault: boolean;
parent: MediaChildParent;
filePath: null | string;
mediaInfo: Next;
type: string;
approved: boolean;
mediaKey: string;
itemFields: any[];
source: Source;
fieldData: Next;
sourceId: null | string;
timecodeOverride: null;
seriesTitle: string;
episodeTitle: string;
genre: any[];
txDate: string;
description: string;
synopsis: string;
resolution: null;
restrictedAccess: boolean;
createdById: string;
userIdsWithAccess: any[];
runtime?: number;
language?: TitleElement;
purchased: boolean;
}
export interface Avail {
id: number;
description: string;
endDate: string;
startDate: string;
ids: AvailIDS;
originalAirDate: null;
physicalReleaseDate: null;
preorderDate: null;
language: TitleElement;
territory: string;
territoryCode: string;
license: string;
parentAvail: null;
item: number;
version: string;
applyToLevel: null;
availLevel: string;
availDisplayCode: string;
availStatus: string;
bundleOnly: boolean;
contentOwnerOrganization: string;
currency: null;
price: null;
purchase: string;
priceValue: string;
resolutionFormat: null;
runtimeMilliseconds: null;
seasonOrEpisodeNumber: null;
tmsid: null;
deviceList: string;
tvodSku: null;
}
export interface AvailIDS {
externalSeasonId: string;
externalAsianId: null;
externalShowId: string;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
}
export type Next = Record<string, unknown>
export interface LanguageClass {
code: string;
id: number;
title: TitleElement;
}
export interface MediaChild {
id: number;
title: string;
experienceType: string;
created: string;
createdBy: string;
itemFieldData: Next;
keyPath: null;
filename: string;
complianceStatus: null;
events: any[];
clients: string[];
qcStatus: null;
qcStatusDate: null;
image: string;
ext: string;
avails: any[];
version: string;
startTimecode: null;
endTimecode: null;
versionId: string;
mediaType: string;
status: string;
languages: LanguageClass[];
territories: any[];
devices: any[];
keyType: string;
purpose: null;
externalItemId: string;
proxyId: null;
externalDbId: null;
mediaChildren: any[];
isDefault: boolean;
parent: MediaChildParent;
filePath: string;
mediaInfo: MediaInfo;
type: string;
approved: boolean;
mediaKey: null;
itemFields: any[];
source: Source;
fieldData: Next;
sourceId: null;
timecodeOverride: null;
seriesTitle: string;
episodeTitle: string;
genre: any[];
txDate: string;
description: string;
synopsis: string;
resolution: null | string;
restrictedAccess: boolean;
createdById: string;
userIdsWithAccess: any[];
language: TitleElement;
}
export interface MediaInfo {
imageAspectRatio: null | string;
format: string;
scanMode: null | string;
burnedInSubtitleLanguage: string;
screenAspectRatio: null | string;
subtitleFormat: null | string;
subtitleContent: null | string;
frameHeight: number | null;
frameWidth: number | null;
video: Video;
}
export interface Video {
codecId: null | string;
container: null | string;
encodingRate: number | null;
frameRate: null | string;
height: number | null;
width: number | null;
duration: number | null;
bitRate: number | null;
}
export interface MediaChildParent {
title: string;
type: string;
catalogParent: CatalogParent;
slug: string;
grandparentId: number;
id: number;
}
export interface CatalogParent {
id: number;
title: string;
}
export enum Source {
Dbb = 'dbb',
}
export interface MetaItems {
items: Items;
filters: Filters;
}
export interface Filters {
territory: any[];
language: any[];
}
export interface Items {
'release-date': AnimationProductionStudio;
rating: AnimationProductionStudio;
synopsis: AnimationProductionStudio;
'animation-production-studio': AnimationProductionStudio;
}
export interface AnimationProductionStudio {
items: AnimationProductionStudioItem[];
label: string;
id: number;
slug: string;
}
export interface AnimationProductionStudioItem {
id: number;
metaType: MetaType;
metaTypeId: string;
client: null;
languages: TitleElement;
territories: string;
devices: string;
isDefault: boolean;
value: Value[];
approved: boolean;
version: null;
source: Source;
}
export interface EpisodeDataParent {
seasonId: number;
seasonNumber: string;
title: string;
titleSlug: string;
titleType: string;
titleId: number;
}
export interface PreviousSeasonEpisode {
seasonTitle?: string;
mediaCategory: Type;
thumb: string;
title: string;
image: string;
number: string;
id: number;
version: string[];
order: number;
slug: string;
season?: number;
languages?: TitleElement[];
}
export enum Type {
Episode = 'episode',
Ova = 'ova',
}
export interface Quality {
quality: string;
height: number;
}
export interface UserRating {
overall: number;
ja: number;
eng: number;
}
export interface VersionClass {
compliance_approved: boolean;
title: string;
version_id: string;
is_default: boolean;
runtime: string;
external_id: string;
id: number;
}

View file

@ -1,34 +1,34 @@
// Generated by https://quicktype.io
export interface FunimationSearch {
count: number;
items: Items;
limit: string;
offset: string;
}
export interface Items {
hits: Hit[];
}
export interface Hit {
ratings: string;
description: string;
title: string;
image: {
showThumbnail: string,
[key: string]: string
};
starRating: number;
slug: string;
languages: string[];
synopsis: string;
quality: Quality;
id: string;
txDate: number;
}
export interface Quality {
quality: string;
height: number;
}
// Generated by https://quicktype.io
export interface FunimationSearch {
count: number;
items: Items;
limit: string;
offset: string;
}
export interface Items {
hits: Hit[];
}
export interface Hit {
ratings: string;
description: string;
title: string;
image: {
showThumbnail: string,
[key: string]: string
};
starRating: number;
slug: string;
languages: string[];
synopsis: string;
quality: Quality;
id: string;
txDate: number;
}
export interface Quality {
quality: string;
height: number;
}

30
@types/funiTypes.d.ts vendored
View file

@ -1,16 +1,16 @@
import { LanguageItem } from '../modules/module.langsData';
export type FunimationMediaDownload = {
id: string,
title: string,
showTitle: string,
image: string
}
export type Subtitle = {
url: string,
lang: LanguageItem,
ext: string,
out?: string,
closedCaption?: boolean
import { LanguageItem } from '../modules/module.langsData';
export type FunimationMediaDownload = {
id: string,
title: string,
showTitle: string,
image: string
}
export type Subtitle = {
url: string,
lang: LanguageItem,
ext: string,
out?: string,
closedCaption?: boolean
}

212
@types/github.d.ts vendored
View file

@ -1,106 +1,106 @@
export type GithubTag = {
name: string,
zipball_url: string,
tarball_url: string,
commit: {
sha: string,
url: string
},
node_id: string
}
export interface TagCompare {
url: string;
html_url: string;
permalink_url: string;
diff_url: string;
patch_url: string;
base_commit: BaseCommitClass;
merge_base_commit: BaseCommitClass;
status: string;
ahead_by: number;
behind_by: number;
total_commits: number;
commits: BaseCommitClass[];
files: File[];
}
export interface BaseCommitClass {
sha: string;
node_id: string;
commit: BaseCommitCommit;
url: string;
html_url: string;
comments_url: string;
author: BaseCommitAuthor;
committer: BaseCommitAuthor;
parents: Parent[];
}
export interface BaseCommitAuthor {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface BaseCommitCommit {
author: PurpleAuthor;
committer: PurpleAuthor;
message: string;
tree: Tree;
url: string;
comment_count: number;
verification: Verification;
}
export interface PurpleAuthor {
name: string;
email: string;
date: string;
}
export interface Tree {
sha: string;
url: string;
}
export interface Verification {
verified: boolean;
reason: string;
signature: string;
payload: string;
}
export interface Parent {
sha: string;
url: string;
html_url: string;
}
export interface File {
sha: string;
filename: string;
status: string;
additions: number;
deletions: number;
changes: number;
blob_url: string;
raw_url: string;
contents_url: string;
patch: string;
}
export type GithubTag = {
name: string,
zipball_url: string,
tarball_url: string,
commit: {
sha: string,
url: string
},
node_id: string
}
export interface TagCompare {
url: string;
html_url: string;
permalink_url: string;
diff_url: string;
patch_url: string;
base_commit: BaseCommitClass;
merge_base_commit: BaseCommitClass;
status: string;
ahead_by: number;
behind_by: number;
total_commits: number;
commits: BaseCommitClass[];
files: File[];
}
export interface BaseCommitClass {
sha: string;
node_id: string;
commit: BaseCommitCommit;
url: string;
html_url: string;
comments_url: string;
author: BaseCommitAuthor;
committer: BaseCommitAuthor;
parents: Parent[];
}
export interface BaseCommitAuthor {
login: string;
id: number;
node_id: string;
avatar_url: string;
gravatar_id: string;
url: string;
html_url: string;
followers_url: string;
following_url: string;
gists_url: string;
starred_url: string;
subscriptions_url: string;
organizations_url: string;
repos_url: string;
events_url: string;
received_events_url: string;
type: string;
site_admin: boolean;
}
export interface BaseCommitCommit {
author: PurpleAuthor;
committer: PurpleAuthor;
message: string;
tree: Tree;
url: string;
comment_count: number;
verification: Verification;
}
export interface PurpleAuthor {
name: string;
email: string;
date: string;
}
export interface Tree {
sha: string;
url: string;
}
export interface Verification {
verified: boolean;
reason: string;
signature: string;
payload: string;
}
export interface Parent {
sha: string;
url: string;
html_url: string;
}
export interface File {
sha: string;
filename: string;
status: string;
additions: number;
deletions: number;
changes: number;
blob_url: string;
raw_url: string;
contents_url: string;
patch: string;
}

View file

@ -1,30 +1,30 @@
declare module 'hls-download' {
import type { ProgressData } from './messageHandler';
export type HLSCallback = (data: ProgressData) => unknown;
export default class hlsDownload {
constructor(options: {
m3u8json: {
segments: Record<string, unknown>[],
mediaSequence?: number,
},
output?: string,
threads?: number,
retries?: number,
offset?: number,
baseurl?: string,
proxy?: string,
skipInit?: boolean,
timeout?: number,
fsRetryTime?: number,
callback?: HLSCallback
})
async download() : Promise<{
ok: boolean,
parts: {
first: number,
total: number,
compleated: number
}
}>
}
declare module 'hls-download' {
import type { ProgressData } from './messageHandler';
export type HLSCallback = (data: ProgressData) => unknown;
export default class hlsDownload {
constructor(options: {
m3u8json: {
segments: Record<string, unknown>[],
mediaSequence?: number,
},
output?: string,
threads?: number,
retries?: number,
offset?: number,
baseurl?: string,
proxy?: string,
skipInit?: boolean,
timeout?: number,
fsRetryTime?: number,
callback?: HLSCallback
})
async download() : Promise<{
ok: boolean,
parts: {
first: number,
total: number,
compleated: number
}
}>
}
}

16
@types/iso639.d.ts vendored
View file

@ -1,9 +1,9 @@
declare module 'iso-639' {
export type iso639Type = {
[key: string]: {
'639-1'?: string,
'639-2'?: string
}
}
export const iso_639_2: iso639Type;
declare module 'iso-639' {
export type iso639Type = {
[key: string]: {
'639-1'?: string,
'639-2'?: string
}
}
export const iso_639_2: iso639Type;
}

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

@ -1,169 +1,169 @@
export interface Item {
// Added later
id: string,
id_split: (number|string)[]
// Added from the start
mostRecentSvodJpnUs: MostRecentSvodJpnUs;
synopsis: string;
mediaCategory: ContentType;
mostRecentSvodUsEndTimestamp: number;
quality: QualityClass;
genres: Genre[];
titleImages: TitleImages;
engAllTerritoryAvail: EngAllTerritoryAvail;
thumb: string;
mostRecentSvodJpnAllTerrStartTimestamp: number;
title: string;
starRating: number;
primaryAvail: PrimaryAvail;
access: Access[];
version: Version[];
mostRecentSvodJpnAllTerrEndTimestamp: number;
itemId: number;
versionAudio: VersionAudio;
contentType: ContentType;
mostRecentSvodUsStartTimestamp: number;
poster: string;
mostRecentSvodEngAllTerrEndTimestamp: number;
mostRecentSvodJpnUsStartTimestamp: number;
mostRecentSvodJpnUsEndTimestamp: number;
mostRecentSvodStartTimestamp: number;
mostRecentSvod: MostRecent;
altAvail: AltAvail;
ids: IDs;
mostRecentSvodUs: MostRecent;
item: Item;
mostRecentSvodEngAllTerrStartTimestamp: number;
audio: string[];
mostRecentAvod: MostRecent;
}
export enum ContentType {
Episode = 'episode',
Ova = 'ova',
}
export interface IDs {
externalShowId: ID;
externalSeasonId: ExternalSeasonID;
externalEpisodeId: string;
externalAsianId?: string
}
export interface Item {
seasonTitle: string;
seasonId: number;
episodeOrder: number;
episodeSlug: string;
created: Date;
titleSlug: string;
episodeNum: string;
episodeId: number;
titleId: number;
seasonNum: string;
ratings: Array<string[]>;
showImage: string;
titleName: string;
runtime: string;
episodeName: string;
seasonOrder: number;
titleExternalId: string;
}
export interface MostRecent {
image?: string;
siblingStartTimestamp?: string;
devices?: Device[];
availId?: number;
distributor?: Distributor;
quality?: MostRecentAvodQuality;
endTimestamp?: string;
mediaCategory?: ContentType;
isPromo?: boolean;
siblingType?: Purchase;
version?: Version;
territory?: Territory;
startDate?: Date;
endDate?: Date;
versionId?: number;
tier?: Device | null;
purchase?: Purchase;
startTimestamp?: string;
language?: Audio;
itemTitle?: string;
ids?: MostRecentAvodIDS;
experience?: number;
siblingEndTimestamp?: string;
item?: Item;
subscriptionRequired?: boolean;
purchased?: boolean;
}
export interface MostRecentAvodIDS {
externalSeasonId: ExternalSeasonID;
externalAsianId: null;
externalShowId: ID;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
}
export enum Purchase {
AVOD = 'A-VOD',
Dfov = 'DFOV',
Est = 'EST',
Svod = 'SVOD',
}
export enum Version {
Simulcast = 'Simulcast',
Uncut = 'Uncut',
}
export type MostRecentSvodJpnUs = Record<string, any>
export interface QualityClass {
quality: QualityQuality;
height: number;
}
export enum QualityQuality {
HD = 'HD',
SD = 'SD',
}
export interface TitleImages {
showThumbnail: string;
showBackgroundSite: string;
showDetailHeaderDesktop: string;
continueWatchingDesktop: string;
showDetailHeroSite: string;
appleHorizontalBannerShow: string;
backgroundImageXbox_360: string;
applePosterCover: string;
showDetailBoxArtTablet: string;
featuredShowBackgroundTablet: string;
backgroundImageAppletvfiretv: string;
newShowDetailHero: string;
showDetailHeroDesktop: string;
showKeyart: string;
continueWatchingMobile: string;
featuredSpotlightShowPhone: string;
appleHorizontalBannerMovie: string;
featuredSpotlightShowTablet: string;
showDetailBoxArtPhone: string;
featuredShowBackgroundPhone: string;
appleSquareCover: string;
backgroundVideo: string;
showMasterKeyArt: string;
newShowDetailHeroPhone: string;
showDetailBoxArtXbox_360: string;
showDetailHeaderMobile: string;
showLogo: string;
}
export interface VersionAudio {
Uncut?: Audio[];
Simulcast: Audio[];
export interface Item {
// Added later
id: string,
id_split: (number|string)[]
// Added from the start
mostRecentSvodJpnUs: MostRecentSvodJpnUs;
synopsis: string;
mediaCategory: ContentType;
mostRecentSvodUsEndTimestamp: number;
quality: QualityClass;
genres: Genre[];
titleImages: TitleImages;
engAllTerritoryAvail: EngAllTerritoryAvail;
thumb: string;
mostRecentSvodJpnAllTerrStartTimestamp: number;
title: string;
starRating: number;
primaryAvail: PrimaryAvail;
access: Access[];
version: Version[];
mostRecentSvodJpnAllTerrEndTimestamp: number;
itemId: number;
versionAudio: VersionAudio;
contentType: ContentType;
mostRecentSvodUsStartTimestamp: number;
poster: string;
mostRecentSvodEngAllTerrEndTimestamp: number;
mostRecentSvodJpnUsStartTimestamp: number;
mostRecentSvodJpnUsEndTimestamp: number;
mostRecentSvodStartTimestamp: number;
mostRecentSvod: MostRecent;
altAvail: AltAvail;
ids: IDs;
mostRecentSvodUs: MostRecent;
item: Item;
mostRecentSvodEngAllTerrStartTimestamp: number;
audio: string[];
mostRecentAvod: MostRecent;
}
export enum ContentType {
Episode = 'episode',
Ova = 'ova',
}
export interface IDs {
externalShowId: ID;
externalSeasonId: ExternalSeasonID;
externalEpisodeId: string;
externalAsianId?: string
}
export interface Item {
seasonTitle: string;
seasonId: number;
episodeOrder: number;
episodeSlug: string;
created: Date;
titleSlug: string;
episodeNum: string;
episodeId: number;
titleId: number;
seasonNum: string;
ratings: Array<string[]>;
showImage: string;
titleName: string;
runtime: string;
episodeName: string;
seasonOrder: number;
titleExternalId: string;
}
export interface MostRecent {
image?: string;
siblingStartTimestamp?: string;
devices?: Device[];
availId?: number;
distributor?: Distributor;
quality?: MostRecentAvodQuality;
endTimestamp?: string;
mediaCategory?: ContentType;
isPromo?: boolean;
siblingType?: Purchase;
version?: Version;
territory?: Territory;
startDate?: Date;
endDate?: Date;
versionId?: number;
tier?: Device | null;
purchase?: Purchase;
startTimestamp?: string;
language?: Audio;
itemTitle?: string;
ids?: MostRecentAvodIDS;
experience?: number;
siblingEndTimestamp?: string;
item?: Item;
subscriptionRequired?: boolean;
purchased?: boolean;
}
export interface MostRecentAvodIDS {
externalSeasonId: ExternalSeasonID;
externalAsianId: null;
externalShowId: ID;
externalEpisodeId: string;
externalEnglishId: string;
externalAlphaId: string;
}
export enum Purchase {
AVOD = 'A-VOD',
Dfov = 'DFOV',
Est = 'EST',
Svod = 'SVOD',
}
export enum Version {
Simulcast = 'Simulcast',
Uncut = 'Uncut',
}
export type MostRecentSvodJpnUs = Record<string, any>
export interface QualityClass {
quality: QualityQuality;
height: number;
}
export enum QualityQuality {
HD = 'HD',
SD = 'SD',
}
export interface TitleImages {
showThumbnail: string;
showBackgroundSite: string;
showDetailHeaderDesktop: string;
continueWatchingDesktop: string;
showDetailHeroSite: string;
appleHorizontalBannerShow: string;
backgroundImageXbox_360: string;
applePosterCover: string;
showDetailBoxArtTablet: string;
featuredShowBackgroundTablet: string;
backgroundImageAppletvfiretv: string;
newShowDetailHero: string;
showDetailHeroDesktop: string;
showKeyart: string;
continueWatchingMobile: string;
featuredSpotlightShowPhone: string;
appleHorizontalBannerMovie: string;
featuredSpotlightShowTablet: string;
showDetailBoxArtPhone: string;
featuredShowBackgroundPhone: string;
appleSquareCover: string;
backgroundVideo: string;
showMasterKeyArt: string;
newShowDetailHeroPhone: string;
showDetailBoxArtXbox_360: string;
showDetailHeaderMobile: string;
showLogo: string;
}
export interface VersionAudio {
Uncut?: Audio[];
Simulcast: Audio[];
}

View file

@ -1,49 +1,49 @@
declare module 'm3u8-parsed' {
export type M3U8 = {
allowCache: boolean,
discontinuityStarts: [],
segments: {
duration: number,
byterange?: {
length: number,
offset: number
},
uri: string,
key: {
method: string,
uri: string,
},
timeline: number
}[],
version: number,
mediaGroups: {
[type: string]: {
[index: string]: {
[language: string]: {
default: boolean,
autoselect: boolean,
language: string,
uri: string
}
}
}
},
playlists: {
uri: string,
timeline: number,
attributes: {
'CLOSED-CAPTIONS': string,
'AUDIO': string,
'FRAME-RATE': number,
'RESOLUTION': {
width: number,
height: number
},
'CODECS': string,
'AVERAGE-BANDWIDTH': string,
'BANDWIDTH': number
}
}[],
}
export default function (data: string): M3U8;
declare module 'm3u8-parsed' {
export type M3U8 = {
allowCache: boolean,
discontinuityStarts: [],
segments: {
duration: number,
byterange?: {
length: number,
offset: number
},
uri: string,
key: {
method: string,
uri: string,
},
timeline: number
}[],
version: number,
mediaGroups: {
[type: string]: {
[index: string]: {
[language: string]: {
default: boolean,
autoselect: boolean,
language: string,
uri: string
}
}
}
},
playlists: {
uri: string,
timeline: number,
attributes: {
'CLOSED-CAPTIONS': string,
'AUDIO': string,
'FRAME-RATE': number,
'RESOLUTION': {
width: number,
height: number
},
'CODECS': string,
'AVERAGE-BANDWIDTH': string,
'BANDWIDTH': number
}
}[],
}
export default function (data: string): M3U8;
}

View file

@ -1,122 +1,122 @@
import { HLSCallback } from 'hls-download';
import type { FunimationSearch } from './funiSearch';
import type { AvailableMuxer } from '../modules/module.args';
export interface MessageHandler {
auth: (data: AuthData) => Promise<AuthResponse>;
checkToken: () => Promise<CheckTokenResponse>;
search: (data: SearchData) => Promise<SearchResponse>,
availableDubCodes: () => Promise<string[]>,
handleDefault: (name: string) => Promise<any>,
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
downloadItem: (data) => void,
isDownloading: () => boolean,
writeToClipboard: (text: string) => void,
openFolder: (path: string[]) => void
}
export type QueueItem = {
title: string,
episode: string,
ids: string[],
fileName: string,
parent: {
title: string,
season: string
},
q: number,
dubLang: string[],
}
export type ResolveItemsData = {
id: string,
dubLang: string[],
all: boolean,
but: boolean,
e: string,
fileName: string,
q: number
}
export type SearchResponseItem = {
image: string,
name: string,
desc?: string,
id: string,
lang?: string[],
rating: number
};
export type Episode = {
e: string,
lang: string[],
name: string,
season: string,
seasonTitle: string,
episode: string,
id: string,
img: string,
description: string,
time: string
}
export type SearchResponse = ResponseBase<SearchResponseItem[]>
export type EpisodeListResponse = ResponseBase<Episode[]>
export type FuniEpisodeData = {
title: string,
episode: string,
episodeID: string,
seasonTitle: string,
seasonNumber: string,
};
export type AuthData = { username: string, password: string };
export type SearchData = { search: string, page?: number, 'search-type'?: string, 'search-locale'?: string };
export type FuniGetShowData = { id: number, e?: string, but: boolean, all: boolean };
export type FuniGetEpisodeData = { subs: FuniSubsData, fnSlug: FuniEpisodeData, simul?: boolean; dubLang: string[], s: string }
export type FuniStreamData = { callbackMaker?: (data: DownloadInfo) => HLSCallback, q: number, x: number, fileName: string, numbers: number, novids?: boolean,
timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean,
forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean }
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[] }
export type DownloadData = { id: string, e: string, dubLang: string[], fileName: string, q: number }
export type AuthResponse = ResponseBase<undefined>;
export type FuniSearchReponse = ResponseBase<FunimationSearch>;
export type FuniShowResponse = ResponseBase<FuniEpisodeData[]>;
export type FuniGetEpisodeResponse = ResponseBase<undefined>;
export type CheckTokenResponse = ResponseBase<undefined>;
export type ResponseBase<T> = ({
isOk: true,
value: T
} | {
isOk: false,
reason: Error
});
export type ProgressData = {
total: number,
cur: number,
percent: number|string,
time: number,
downloadSpeed: number
};
export type PossibleMessanges = keyof ServiceHandler;
export type DownloadInfo = {
image: string,
parent: {
title: string
},
title: string,
fileName: string
}
export type ExtendedProgress = {
progress: ProgressData,
downloadInfo: DownloadInfo
import { HLSCallback } from 'hls-download';
import type { FunimationSearch } from './funiSearch';
import type { AvailableMuxer } from '../modules/module.args';
export interface MessageHandler {
auth: (data: AuthData) => Promise<AuthResponse>;
checkToken: () => Promise<CheckTokenResponse>;
search: (data: SearchData) => Promise<SearchResponse>,
availableDubCodes: () => Promise<string[]>,
handleDefault: (name: string) => Promise<any>,
resolveItems: (data: ResolveItemsData) => Promise<ResponseBase<QueueItem[]>>,
listEpisodes: (id: string) => Promise<EpisodeListResponse>,
downloadItem: (data) => void,
isDownloading: () => boolean,
writeToClipboard: (text: string) => void,
openFolder: (path: string[]) => void
}
export type QueueItem = {
title: string,
episode: string,
ids: string[],
fileName: string,
parent: {
title: string,
season: string
},
q: number,
dubLang: string[],
}
export type ResolveItemsData = {
id: string,
dubLang: string[],
all: boolean,
but: boolean,
e: string,
fileName: string,
q: number
}
export type SearchResponseItem = {
image: string,
name: string,
desc?: string,
id: string,
lang?: string[],
rating: number
};
export type Episode = {
e: string,
lang: string[],
name: string,
season: string,
seasonTitle: string,
episode: string,
id: string,
img: string,
description: string,
time: string
}
export type SearchResponse = ResponseBase<SearchResponseItem[]>
export type EpisodeListResponse = ResponseBase<Episode[]>
export type FuniEpisodeData = {
title: string,
episode: string,
episodeID: string,
seasonTitle: string,
seasonNumber: string,
};
export type AuthData = { username: string, password: string };
export type SearchData = { search: string, page?: number, 'search-type'?: string, 'search-locale'?: string };
export type FuniGetShowData = { id: number, e?: string, but: boolean, all: boolean };
export type FuniGetEpisodeData = { subs: FuniSubsData, fnSlug: FuniEpisodeData, simul?: boolean; dubLang: string[], s: string }
export type FuniStreamData = { callbackMaker?: (data: DownloadInfo) => HLSCallback, q: number, x: number, fileName: string, numbers: number, novids?: boolean,
timeout: number, partsize: number, fsRetryTime: number, noaudio?: boolean, mp4: boolean, ass: boolean, fontSize: number, fontName?: string, skipmux?: boolean,
forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean }
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[] }
export type DownloadData = { id: string, e: string, dubLang: string[], fileName: string, q: number }
export type AuthResponse = ResponseBase<undefined>;
export type FuniSearchReponse = ResponseBase<FunimationSearch>;
export type FuniShowResponse = ResponseBase<FuniEpisodeData[]>;
export type FuniGetEpisodeResponse = ResponseBase<undefined>;
export type CheckTokenResponse = ResponseBase<undefined>;
export type ResponseBase<T> = ({
isOk: true,
value: T
} | {
isOk: false,
reason: Error
});
export type ProgressData = {
total: number,
cur: number,
percent: number|string,
time: number,
downloadSpeed: number
};
export type PossibleMessanges = keyof ServiceHandler;
export type DownloadInfo = {
image: string,
parent: {
title: string
},
title: string,
fileName: string
}
export type ExtendedProgress = {
progress: ProgressData,
downloadInfo: DownloadInfo
}

186
@types/objectInfo.d.ts vendored
View file

@ -1,93 +1,93 @@
// Generated by https://quicktype.io
export interface ObjectInfo {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: unknown;
__actions__: unknown;
total: number;
items: Item[];
}
export interface Item {
__class__: string;
__href__: string;
__links__: Links;
__actions__: unknown;
id: string;
external_id: string;
channel_id: string;
title: string;
description: string;
promo_title: string;
promo_description: string;
type: string;
slug: string;
slug_title: string;
images: Images;
episode_metadata: EpisodeMetadata;
playback: string;
linked_resource_key: string;
type: string;
s_num?: string;
f_num?: string;
movie_metadata?: {
movie_listing_id: string;
movie_listing_title: string
};
isSelected?: boolean
}
export interface Links {
'episode/season': EpisodeSeason;
'episode/series': EpisodeSeason;
resource: EpisodeSeason;
'resource/channel': EpisodeSeason;
streams: EpisodeSeason;
}
export interface EpisodeSeason {
href: string;
}
export interface EpisodeMetadata {
series_id: string;
series_title: string;
series_slug_title: string;
season_id: string;
season_title: string;
season_slug_title: string;
season_number: number;
episode_number: number;
episode: string;
sequence_number: number;
duration_ms: number;
ad_breaks: AdBreak[];
episode_air_date: string;
is_premium_only: boolean;
is_mature: boolean;
mature_blocked: boolean;
is_subbed: boolean;
is_dubbed: boolean;
is_clip: boolean;
available_offline: boolean;
maturity_ratings: string[];
subtitle_locales: string[];
availability_notes: string;
}
export interface AdBreak {
type: string;
offset_ms: number;
}
export interface Images {
thumbnail: Array<Thumbnail[]>;
}
export interface Thumbnail {
width: number;
height: number;
type: string;
source: string;
}
// Generated by https://quicktype.io
export interface ObjectInfo {
__class__: string;
__href__: string;
__resource_key__: string;
__links__: unknown;
__actions__: unknown;
total: number;
items: Item[];
}
export interface Item {
__class__: string;
__href__: string;
__links__: Links;
__actions__: unknown;
id: string;
external_id: string;
channel_id: string;
title: string;
description: string;
promo_title: string;
promo_description: string;
type: string;
slug: string;
slug_title: string;
images: Images;
episode_metadata: EpisodeMetadata;
playback: string;
linked_resource_key: string;
type: string;
s_num?: string;
f_num?: string;
movie_metadata?: {
movie_listing_id: string;
movie_listing_title: string
};
isSelected?: boolean
}
export interface Links {
'episode/season': EpisodeSeason;
'episode/series': EpisodeSeason;
resource: EpisodeSeason;
'resource/channel': EpisodeSeason;
streams: EpisodeSeason;
}
export interface EpisodeSeason {
href: string;
}
export interface EpisodeMetadata {
series_id: string;
series_title: string;
series_slug_title: string;
season_id: string;
season_title: string;
season_slug_title: string;
season_number: number;
episode_number: number;
episode: string;
sequence_number: number;
duration_ms: number;
ad_breaks: AdBreak[];
episode_air_date: string;
is_premium_only: boolean;
is_mature: boolean;
mature_blocked: boolean;
is_subbed: boolean;
is_dubbed: boolean;
is_clip: boolean;
available_offline: boolean;
maturity_ratings: string[];
subtitle_locales: string[];
availability_notes: string;
}
export interface AdBreak {
type: string;
offset_ms: number;
}
export interface Images {
thumbnail: Array<Thumbnail[]>;
}
export interface Thumbnail {
width: number;
height: number;
type: string;
source: string;
}

4
@types/pkg.d.ts vendored
View file

@ -1,3 +1,3 @@
declare module 'pkg' {
export async function exec(config: string[]);
declare module 'pkg' {
export async function exec(config: string[]);
}

View file

@ -1,34 +1,34 @@
// Generated by https://quicktype.io
export interface PlaybackData {
audio_locale: string;
subtitles: { [key: string]: Subtitle };
streams: { [key: string]: { [key: string]: Stream } };
QoS: QoS;
}
export interface QoS {
region: string;
cloudFrontRequestId: string;
lambdaRunTime: number;
}
export interface Stream {
hardsub_locale: string;
url: string;
vcodec: Vcodec;
hardsub_lang?: string;
audio_lang?: string;
type?: string;
}
export enum Vcodec {
H264 = 'h264',
}
export interface Subtitle {
locale: Locale;
url: string;
format: string;
}
// Generated by https://quicktype.io
export interface PlaybackData {
audio_locale: string;
subtitles: { [key: string]: Subtitle };
streams: { [key: string]: { [key: string]: Stream } };
QoS: QoS;
}
export interface QoS {
region: string;
cloudFrontRequestId: string;
lambdaRunTime: number;
}
export interface Stream {
hardsub_locale: string;
url: string;
vcodec: Vcodec;
hardsub_lang?: string;
audio_lang?: string;
type?: string;
}
export enum Vcodec {
H264 = 'h264',
}
export interface Subtitle {
locale: Locale;
url: string;
format: string;
}

View file

@ -1,13 +1,13 @@
import { ExtendedProgress } from "./messageHandler";
export type RandomEvents = {
progress: ExtendedProgress,
finish: undefined
}
export interface RandomEvent<T extends keyof RandomEvents> {
name: T,
data: RandomEvents[T]
}
import { ExtendedProgress } from './messageHandler';
export type RandomEvents = {
progress: ExtendedProgress,
finish: undefined
}
export interface RandomEvent<T extends keyof RandomEvents> {
name: T,
data: RandomEvents[T]
}
export type Handler<T extends keyof RandomEvents> = (data: RandomEvent<T>) => unknown;

View file

@ -1,3 +1,3 @@
declare module 'removeNPMAbsolutePaths' {
export default async function modulesCleanup(path: string);
declare module 'removeNPMAbsolutePaths' {
export default async function modulesCleanup(path: string);
}

View file

@ -1,15 +1,15 @@
declare module 'sei-helper' {
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
declare module 'sei-helper' {
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,3 +1,3 @@
export interface ServiceClass {
cli: () => Promise<boolean|undefined|void>
export interface ServiceClass {
cli: () => Promise<boolean|undefined|void>
}

View file

@ -1,28 +1,28 @@
// Generated by https://quicktype.io
export interface StreamData {
items: Item[];
watchHistorySaveInterval: number;
errors?: Error[]
}
export interface Error {
detail: string,
code: number
}
export interface Item {
src: string;
kind: string;
isPromo: boolean;
videoType: string;
aips: Aip[];
experienceId: string;
showAds: boolean;
id: number;
}
export interface Aip {
out: number;
in: number;
}
// Generated by https://quicktype.io
export interface StreamData {
items: Item[];
watchHistorySaveInterval: number;
errors?: Error[]
}
export interface Error {
detail: string,
code: number
}
export interface Item {
src: string;
kind: string;
isPromo: boolean;
videoType: string;
aips: Aip[];
experienceId: string;
showAds: boolean;
id: number;
}
export interface Aip {
out: number;
in: number;
}

View file

@ -1,4 +1,4 @@
export type UpdateFile = {
lastCheck: number,
nextCheck: number
export type UpdateFile = {
lastCheck: number,
nextCheck: number
}

3092
crunchy.ts

File diff suppressed because it is too large Load diff

1728
funi.ts

File diff suppressed because it is too large Load diff

View file

@ -1,159 +1,160 @@
import { app, BrowserWindow, dialog, } from 'electron';
import path from 'path/posix';
import registerMessageHandler from './messageHandler';
import fs from 'fs';
import dotenv from 'dotenv';
import express from "express";
import { Console } from 'console';
import json from '../../../package.json';
process.on('uncaughtException', (er, or) => {
console.error(er, or);
});
process.on('unhandledRejection', (er, pr) => {
console.log(er, pr);
});
const getDataDirectory = () => {
switch (process.platform) {
case "darwin": {
if (!process.env.HOME) {
console.error('Unknown home directory');
process.exit(1);
}
return path.join(process.env.HOME, "Library", "Application Support", json.name);
}
case "win32": {
if (!process.env.APPDATA) {
console.error('Unknown home directory');
process.exit(1);
}
return path.join(process.env.APPDATA, json.name);
}
case "linux": {
if (!process.env.HOME) {
console.error('Unknown home directory');
process.exit(1);
}
return path.join(process.env.HOME, `.${json.name}`);
}
default: {
console.error("Unsupported platform!");
process.exit(1);
}
}
}
if (!fs.existsSync(getDataDirectory()))
fs.mkdirSync(getDataDirectory());
export { getDataDirectory };
import './menu';
if (fs.existsSync(path.join(__dirname, '.env')))
dotenv.config({ path: path.join(__dirname, '.env'), debug: true });
if (require('electron-squirrel-startup')) {
app.quit();
}
export const isWindows = process.platform === 'win32';
let mainWindow: BrowserWindow|undefined = undefined;
export { mainWindow };
const icon = path.join(__dirname, 'images', `Logo_Inverted.${isWindows ? 'ico' : 'png'}`);
if (!process.env.TEST) {
console = (() => {
const logFolder = path.join(getDataDirectory(), 'logs');
if (!fs.existsSync(logFolder))
fs.mkdirSync(logFolder);
return new Console(fs.createWriteStream(path.join(logFolder, `${Date.now()}.log`)));
})();
}
const createWindow = async () => {
// Create the browser window.
mainWindow = new BrowserWindow({
height: 600,
width: 800,
title: 'AniDL GUI BETA',
webPreferences: {
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js')
},
icon,
});
mainWindow.webContents.on('crashed', (e) => console.log(e));
if (!process.env.TEST) {
const response = dialog.showMessageBoxSync(mainWindow, {
title: 'Test Version Information',
message: 'I understand that this is a test version that is subject to changes and most certainly contains errors.'
+ '\nI understand that I am using this tool at my own risk.'
+ '\nI know that bugs or suggestions should be made to Izuco on Discord or under github@izuco.dev'
+ '\nI understand that I should thank Darekon for the art works and the concept art if I see him',
buttons: [
'Cancel',
'I understand'
],
type: 'info'
});
console.log(`[INFO] Popup response: ${response}`);
if (response !== 1 && response !== -1)
app.quit();
}
registerMessageHandler(mainWindow);
if (!process.env.USE_BROWSER) {
const app = express();
// Path.sep seems to return / on windows with electron
// \\ in Filename on Linux is possible but I don't see another way rn
const sep = isWindows ? '\\' : '/';
const p = __dirname.split(sep);
p.pop();
p.push('build');
console.log(p.join(sep));
app.use(express.static(p.join(sep)));
await new Promise((resolve) => {
app.listen(3000, () => {
console.log('Express started');
resolve(undefined);
});
})
}
mainWindow.loadURL('http://localhost:3000');
if (process.env.TEST)
mainWindow.webContents.openDevTools();
};
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('quit', () => {
process.exit(0);
})
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
import { app, BrowserWindow, dialog, } from 'electron';
import path from 'path/posix';
import registerMessageHandler from './messageHandler';
import fs from 'fs';
import dotenv from 'dotenv';
import express from 'express';
import { Console } from 'console';
import json from '../../../package.json';
process.on('uncaughtException', (er, or) => {
console.error(er, or);
});
process.on('unhandledRejection', (er, pr) => {
console.log(er, pr);
});
const getDataDirectory = () => {
switch (process.platform) {
case 'darwin': {
if (!process.env.HOME) {
console.error('Unknown home directory');
process.exit(1);
}
return path.join(process.env.HOME, 'Library', 'Application Support', json.name);
}
case 'win32': {
if (!process.env.APPDATA) {
console.error('Unknown home directory');
process.exit(1);
}
return path.join(process.env.APPDATA, json.name);
}
case 'linux': {
if (!process.env.HOME) {
console.error('Unknown home directory');
process.exit(1);
}
return path.join(process.env.HOME, `.${json.name}`);
}
default: {
console.error('Unsupported platform!');
process.exit(1);
}
}
};
if (!fs.existsSync(getDataDirectory()))
fs.mkdirSync(getDataDirectory());
export { getDataDirectory };
import './menu';
if (fs.existsSync(path.join(__dirname, '.env')))
dotenv.config({ path: path.join(__dirname, '.env'), debug: true });
if (require('electron-squirrel-startup')) {
app.quit();
}
export const isWindows = process.platform === 'win32';
let mainWindow: BrowserWindow|undefined = undefined;
export { mainWindow };
const icon = path.join(__dirname, 'images', `Logo_Inverted.${isWindows ? 'ico' : 'png'}`);
if (!process.env.TEST) {
// eslint-disable-next-line no-global-assign
console = (() => {
const logFolder = path.join(getDataDirectory(), 'logs');
if (!fs.existsSync(logFolder))
fs.mkdirSync(logFolder);
return new Console(fs.createWriteStream(path.join(logFolder, `${Date.now()}.log`)));
})();
}
const createWindow = async () => {
// Create the browser window.
mainWindow = new BrowserWindow({
height: 600,
width: 800,
title: 'AniDL GUI BETA',
webPreferences: {
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js')
},
icon,
});
mainWindow.webContents.on('crashed', (e) => console.log(e));
if (!process.env.TEST) {
const response = dialog.showMessageBoxSync(mainWindow, {
title: 'Test Version Information',
message: 'I understand that this is a test version that is subject to changes and most certainly contains errors.'
+ '\nI understand that I am using this tool at my own risk.'
+ '\nI know that bugs or suggestions should be made to Izuco on Discord or under github@izuco.dev'
+ '\nI understand that I should thank Darekon for the art works and the concept art if I see him',
buttons: [
'Cancel',
'I understand'
],
type: 'info'
});
console.log(`[INFO] Popup response: ${response}`);
if (response !== 1 && response !== -1)
app.quit();
}
registerMessageHandler(mainWindow);
if (!process.env.USE_BROWSER) {
const app = express();
// Path.sep seems to return / on windows with electron
// \\ in Filename on Linux is possible but I don't see another way rn
const sep = isWindows ? '\\' : '/';
const p = __dirname.split(sep);
p.pop();
p.push('build');
console.log(p.join(sep));
app.use(express.static(p.join(sep)));
await new Promise((resolve) => {
app.listen(3000, () => {
console.log('Express started');
resolve(undefined);
});
});
}
mainWindow.loadURL('http://localhost:3000');
if (process.env.TEST)
mainWindow.webContents.openDevTools();
};
app.on('ready', createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('quit', () => {
process.exit(0);
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

View file

@ -1,85 +1,85 @@
import { Menu, MenuItem, MenuItemConstructorOptions, shell } from "electron";
import path from 'path';
import { getDataDirectory } from ".";
import json from '../../../package.json';
const template: (MenuItemConstructorOptions | MenuItem)[] = [
{
label: 'Edit',
submenu: [
{
role: 'undo'
},
{
role: 'redo'
},
{
type: 'separator'
},
{
role: 'cut'
},
{
role: 'copy'
},
{
role: 'paste'
}
]
},
{
label: 'Debug',
submenu: [
{
role: 'toggleDevTools'
},
{
label: 'Open log folder',
click: () => {
shell.openPath(path.join(getDataDirectory(), 'logs'))
}
},
{
role: 'forceReload'
}
]
},
{
label: 'Help',
submenu: [
{
label: 'Version',
sublabel: json.version
},
{
label: 'GitHub',
click: () => {
shell.openExternal('https://github.com/anidl/multi-downloader-nx')
}
},
{
label: 'Report a Bug',
click: () => {
shell.openExternal(`https://github.com/anidl/multi-downloader-nx/issues/new?assignees=izu-co&labels=bug&template=bug.yml&title=BUG&version=${json.version}`)
}
},
{
type: 'separator'
},
{
label: 'Contributors',
click: () => {
shell.openExternal('https://github.com/anidl/multi-downloader-nx/graphs/contributors')
}
},
{
label: 'Discord',
click: () => {
shell.openExternal('https://discord.gg/qEpbWen5vq')
}
}
]
}
]
import { Menu, MenuItem, MenuItemConstructorOptions, shell } from 'electron';
import path from 'path';
import { getDataDirectory } from '.';
import json from '../../../package.json';
const template: (MenuItemConstructorOptions | MenuItem)[] = [
{
label: 'Edit',
submenu: [
{
role: 'undo'
},
{
role: 'redo'
},
{
type: 'separator'
},
{
role: 'cut'
},
{
role: 'copy'
},
{
role: 'paste'
}
]
},
{
label: 'Debug',
submenu: [
{
role: 'toggleDevTools'
},
{
label: 'Open log folder',
click: () => {
shell.openPath(path.join(getDataDirectory(), 'logs'));
}
},
{
role: 'forceReload'
}
]
},
{
label: 'Help',
submenu: [
{
label: 'Version',
sublabel: json.version
},
{
label: 'GitHub',
click: () => {
shell.openExternal('https://github.com/anidl/multi-downloader-nx');
}
},
{
label: 'Report a Bug',
click: () => {
shell.openExternal(`https://github.com/anidl/multi-downloader-nx/issues/new?assignees=izu-co&labels=bug&template=bug.yml&title=BUG&version=${json.version}`);
}
},
{
type: 'separator'
},
{
label: 'Contributors',
click: () => {
shell.openExternal('https://github.com/anidl/multi-downloader-nx/graphs/contributors');
}
},
{
label: 'Discord',
click: () => {
shell.openExternal('https://discord.gg/qEpbWen5vq');
}
}
]
}
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));

View file

@ -1,28 +1,28 @@
import { BrowserWindow, ipcMain } from 'electron';
import { MessageHandler } from '../../../@types/messageHandler';
import Crunchy from './serviceHandler/crunchyroll';
import Funimation from './serviceHandler/funimation';
export default (window: BrowserWindow) => {
let handler: MessageHandler|undefined;
ipcMain.handle('setup', (_, data) => {
if (data === 'funi') {
handler = new Funimation(window);
} else if (data === 'crunchy') {
handler = new Crunchy(window);
}
});
ipcMain.handle('type', async () => handler === undefined ? undefined : handler instanceof Funimation ? 'funi' : 'crunchy');
ipcMain.handle('auth', async (_, data) => handler?.auth(data));
ipcMain.handle('checkToken', async () => handler?.checkToken());
ipcMain.handle('search', async (_, data) => handler?.search(data));
ipcMain.handle('default', async (_, data) => handler?.handleDefault(data));
ipcMain.handle('availableDubCodes', async () => handler?.availableDubCodes());
ipcMain.handle('resolveItems', async (_, data) => handler?.resolveItems(data));
ipcMain.handle('listEpisodes', async (_, data) => handler?.listEpisodes(data));
ipcMain.handle('downloadItem', async (_, data) => handler?.downloadItem(data));
ipcMain.handle('writeToClipboard', async (_, data) => handler?.writeToClipboard(data));
ipcMain.handle('openFolder', async (_, data) => handler?.openFolder(data));
ipcMain.on('isDownloading', (ev) => ev.returnValue = handler?.isDownloading());
};
import { BrowserWindow, ipcMain } from 'electron';
import { MessageHandler } from '../../../@types/messageHandler';
import Crunchy from './serviceHandler/crunchyroll';
import Funimation from './serviceHandler/funimation';
export default (window: BrowserWindow) => {
let handler: MessageHandler|undefined;
ipcMain.handle('setup', (_, data) => {
if (data === 'funi') {
handler = new Funimation(window);
} else if (data === 'crunchy') {
handler = new Crunchy(window);
}
});
ipcMain.handle('type', async () => handler === undefined ? undefined : handler instanceof Funimation ? 'funi' : 'crunchy');
ipcMain.handle('auth', async (_, data) => handler?.auth(data));
ipcMain.handle('checkToken', async () => handler?.checkToken());
ipcMain.handle('search', async (_, data) => handler?.search(data));
ipcMain.handle('default', async (_, data) => handler?.handleDefault(data));
ipcMain.handle('availableDubCodes', async () => handler?.availableDubCodes());
ipcMain.handle('resolveItems', async (_, data) => handler?.resolveItems(data));
ipcMain.handle('listEpisodes', async (_, data) => handler?.listEpisodes(data));
ipcMain.handle('downloadItem', async (_, data) => handler?.downloadItem(data));
ipcMain.handle('writeToClipboard', async (_, data) => handler?.writeToClipboard(data));
ipcMain.handle('openFolder', async (_, data) => handler?.openFolder(data));
ipcMain.on('isDownloading', (ev) => ev.returnValue = handler?.isDownloading());
};

View file

@ -1,14 +1,14 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('Electron', {
ipcRenderer: {
...ipcRenderer,
on: (name: string, handler: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
ipcRenderer.on(name, handler);
return ipcRenderer;
},
removeListener: (name: string, handler: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
ipcRenderer.removeListener(name, handler);
}
}
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('Electron', {
ipcRenderer: {
...ipcRenderer,
on: (name: string, handler: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
ipcRenderer.on(name, handler);
return ipcRenderer;
},
removeListener: (name: string, handler: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
ipcRenderer.removeListener(name, handler);
}
}
});

View file

@ -1,66 +1,66 @@
import { BrowserWindow, clipboard, dialog, shell } from "electron";
import { DownloadInfo, ProgressData } from "../../../../@types/messageHandler";
import { RandomEvent, RandomEvents } from "../../../../@types/randomEvents";
import { isWindows } from "..";
export default class Base {
constructor(private window: BrowserWindow) {}
private downloading = false;
setDownloading(downloading: boolean) {
this.downloading = downloading;
}
getDownloading() {
return this.downloading;
}
alertError(error: Error) {
dialog.showMessageBoxSync(this.window, {
message: `${error.name ?? 'An error occured'}\n${error.message}`,
detail: error.stack,
title: `Error`,
type: 'error'
})
}
makeProgressHandler(videoInfo: DownloadInfo) {
return ((data: ProgressData) => {
this.sendMessage({
name: 'progress',
data: {
downloadInfo: videoInfo,
progress: data
}
})
}).bind(this);
}
getWindow() {
return this.window;
}
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
this.window.webContents.send('randomEvent', data);
}
isDownloading() {
return this.downloading;
}
async writeToClipboard(text: string) {
clipboard.writeText(text, 'clipboard');
return true;
}
async openFolder(subPath: string[]) {
const sep = isWindows ? '\\' : '/';
const p = __dirname.split(sep).slice(0, -4); // gui/electron/src/serviceHandler
p.push(...subPath);
shell.openPath(p.join(sep));
}
import { BrowserWindow, clipboard, dialog, shell } from 'electron';
import { DownloadInfo, ProgressData } from '../../../../@types/messageHandler';
import { RandomEvent, RandomEvents } from '../../../../@types/randomEvents';
import { isWindows } from '..';
export default class Base {
constructor(private window: BrowserWindow) {}
private downloading = false;
setDownloading(downloading: boolean) {
this.downloading = downloading;
}
getDownloading() {
return this.downloading;
}
alertError(error: Error) {
dialog.showMessageBoxSync(this.window, {
message: `${error.name ?? 'An error occured'}\n${error.message}`,
detail: error.stack,
title: 'Error',
type: 'error'
});
}
makeProgressHandler(videoInfo: DownloadInfo) {
return ((data: ProgressData) => {
this.sendMessage({
name: 'progress',
data: {
downloadInfo: videoInfo,
progress: data
}
});
}).bind(this);
}
getWindow() {
return this.window;
}
sendMessage<T extends keyof RandomEvents>(data: RandomEvent<T>) {
this.window.webContents.send('randomEvent', data);
}
isDownloading() {
return this.downloading;
}
async writeToClipboard(text: string) {
clipboard.writeText(text, 'clipboard');
return true;
}
async openFolder(subPath: string[]) {
const sep = isWindows ? '\\' : '/';
const p = __dirname.split(sep).slice(0, -4); // gui/electron/src/serviceHandler
p.push(...subPath);
shell.openPath(p.join(sep));
}
}

View file

@ -1,93 +1,93 @@
import { BrowserWindow } from 'electron';
import { CrunchyDownloadOptions } from '../../../../@types/crunchyTypes';
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import Crunchy from '../../../../crunchy';
import Funimation from '../../../../funi';
import { ArgvType } from '../../../../modules/module.app-args';
import { buildDefault, getDefault } from '../../../../modules/module.args';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
import Base from './base';
class CrunchyHandler extends Base implements MessageHandler {
private crunchy: Crunchy;
constructor(window: BrowserWindow) {
super(window);
this.crunchy = new Crunchy();
}
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
}
public async handleDefault(name: string) {
return getDefault(name, this.crunchy.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
return dubLanguageCodes;
}
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
if (!res.isOk)
return res;
return { isOk: true, value: res.value.map(a => {
return {
...data,
ids: a.data.map(a => a.mediaId),
title: a.episodeTitle,
parent: {
title: a.seasonTitle,
season: a.season.toString()
},
e: a.e,
episode: a.episodeNumber
};
}) };
}
public async search(data: SearchData): Promise<SearchResponse> {
this.crunchy.refreshToken();
const crunchySearch = await this.crunchy.doSearch(data);
if (!crunchySearch.isOk)
return crunchySearch;
return { isOk: true, value: crunchySearch.value };
}
public async checkToken(): Promise<CheckTokenResponse> {
if (await this.crunchy.getProfile()) {
return { isOk: true, value: undefined };
} else {
return { isOk: false, reason: new Error('') };
}
}
public auth(data: AuthData) {
return this.crunchy.doAuth(data);
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
const _default = buildDefault() as ArgvType;
await this.crunchy.refreshToken();
const res = await this.crunchy.downloadFromSeriesID(data.id, {
dubLang: data.dubLang,
e: data.e
});
if (res.isOk) {
for (const select of res.value) {
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
}
}
} else {
this.alertError(res.reason);
}
this.sendMessage({ name: 'finish', data: undefined })
this.setDownloading(false);
}
}
import { BrowserWindow } from 'electron';
import { CrunchyDownloadOptions } from '../../../../@types/crunchyTypes';
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import Crunchy from '../../../../crunchy';
import Funimation from '../../../../funi';
import { ArgvType } from '../../../../modules/module.app-args';
import { buildDefault, getDefault } from '../../../../modules/module.args';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
import Base from './base';
class CrunchyHandler extends Base implements MessageHandler {
private crunchy: Crunchy;
constructor(window: BrowserWindow) {
super(window);
this.crunchy = new Crunchy();
}
public async listEpisodes (id: string): Promise<EpisodeListResponse> {
return { isOk: true, value: (await this.crunchy.listSeriesID(id)).list };
}
public async handleDefault(name: string) {
return getDefault(name, this.crunchy.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
return dubLanguageCodes;
}
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
const res = await this.crunchy.downloadFromSeriesID(data.id, data);
if (!res.isOk)
return res;
return { isOk: true, value: res.value.map(a => {
return {
...data,
ids: a.data.map(a => a.mediaId),
title: a.episodeTitle,
parent: {
title: a.seasonTitle,
season: a.season.toString()
},
e: a.e,
episode: a.episodeNumber
};
}) };
}
public async search(data: SearchData): Promise<SearchResponse> {
this.crunchy.refreshToken();
const crunchySearch = await this.crunchy.doSearch(data);
if (!crunchySearch.isOk)
return crunchySearch;
return { isOk: true, value: crunchySearch.value };
}
public async checkToken(): Promise<CheckTokenResponse> {
if (await this.crunchy.getProfile()) {
return { isOk: true, value: undefined };
} else {
return { isOk: false, reason: new Error('') };
}
}
public auth(data: AuthData) {
return this.crunchy.doAuth(data);
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
const _default = buildDefault() as ArgvType;
await this.crunchy.refreshToken();
const res = await this.crunchy.downloadFromSeriesID(data.id, {
dubLang: data.dubLang,
e: data.e
});
if (res.isOk) {
for (const select of res.value) {
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName }))) {
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
er.name = 'Download error';
this.alertError(er);
}
}
} else {
this.alertError(res.reason);
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
}
}
export default CrunchyHandler;

View file

@ -1,101 +1,101 @@
import { BrowserWindow } from 'electron';
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import Funimation from '../../../../funi';
import { ArgvType } from '../../../../modules/module.app-args';
import { buildDefault, getDefault } from '../../../../modules/module.args';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
import Base from './base';
class FunimationHandler extends Base implements MessageHandler {
private funi: Funimation;
constructor(window: BrowserWindow) {
super(window);
this.funi = new Funimation();
}
public async listEpisodes (id: string) : Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.funi.listShowItems(parse);
if (!request.isOk)
return request;
return { isOk: true, value: request.value.map(item => ({
e: item.id_split.join(''),
lang: item.audio ?? [],
name: item.title,
season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle,
seasonTitle: item.seasonTitle,
episode: item.episodeNum,
id: item.id,
img: item.thumb,
description: item.synopsis,
time: item.runtime ?? item.item.runtime
})) }
}
public async handleDefault(name: string) {
return getDefault(name, this.funi.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
return dubLanguageCodes;
}
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
if (!res.isOk)
return res;
return { isOk: true, value: res.value.map(a => {
return {
...data,
ids: [a.episodeID],
title: a.title,
parent: {
title: a.seasonTitle,
season: a.seasonNumber
},
e: a.episodeID,
episode: a.seasonNumber
};
}) };
}
public async search(data: SearchData): Promise<SearchResponse> {
const funiSearch = await this.funi.searchShow(false, data);
if (!funiSearch.isOk)
return funiSearch;
return { isOk: true, value: funiSearch.value.items.hits.map(a => ({
image: a.image.showThumbnail,
name: a.title,
desc: a.description,
id: a.id,
lang: a.languages,
rating: a.starRating
})) };
}
public async checkToken(): Promise<CheckTokenResponse> {
return this.funi.checkToken();
}
public auth(data: AuthData) {
return this.funi.auth(data);
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
const res = await this.funi.getShow(false, { all: false, but: false, id: parseInt(data.id), e: data.e });
const _default = buildDefault() as ArgvType;
if (!res.isOk)
return this.alertError(res.reason);
for (const ep of res.value) {
await this.funi.getEpisode(false, { dubLang: data.dubLang, fnSlug: ep, s: data.id, subs: { dlsubs: ['all'], sub: false } }, { ..._default, callbackMaker: this.makeProgressHandler.bind(this), ass: true, fileName: data.fileName, q: data.q })
}
this.sendMessage({ name: 'finish', data: undefined })
this.setDownloading(false);
};
}
import { BrowserWindow } from 'electron';
import { AuthData, CheckTokenResponse, DownloadData, EpisodeListResponse, MessageHandler, QueueItem, ResolveItemsData, ResponseBase, SearchData, SearchResponse } from '../../../../@types/messageHandler';
import Funimation from '../../../../funi';
import { ArgvType } from '../../../../modules/module.app-args';
import { buildDefault, getDefault } from '../../../../modules/module.args';
import { dubLanguageCodes } from '../../../../modules/module.langsData';
import Base from './base';
class FunimationHandler extends Base implements MessageHandler {
private funi: Funimation;
constructor(window: BrowserWindow) {
super(window);
this.funi = new Funimation();
}
public async listEpisodes (id: string) : Promise<EpisodeListResponse> {
const parse = parseInt(id);
if (isNaN(parse) || parse <= 0)
return { isOk: false, reason: new Error('The ID is invalid') };
const request = await this.funi.listShowItems(parse);
if (!request.isOk)
return request;
return { isOk: true, value: request.value.map(item => ({
e: item.id_split.join(''),
lang: item.audio ?? [],
name: item.title,
season: item.seasonNum ?? item.seasonTitle ?? item.item.seasonNum ?? item.item.seasonTitle,
seasonTitle: item.seasonTitle,
episode: item.episodeNum,
id: item.id,
img: item.thumb,
description: item.synopsis,
time: item.runtime ?? item.item.runtime
})) };
}
public async handleDefault(name: string) {
return getDefault(name, this.funi.cfg.cli);
}
public async availableDubCodes(): Promise<string[]> {
return dubLanguageCodes;
}
public async resolveItems(data: ResolveItemsData): Promise<ResponseBase<QueueItem[]>> {
const res = await this.funi.getShow(false, { ...data, id: parseInt(data.id) });
if (!res.isOk)
return res;
return { isOk: true, value: res.value.map(a => {
return {
...data,
ids: [a.episodeID],
title: a.title,
parent: {
title: a.seasonTitle,
season: a.seasonNumber
},
e: a.episodeID,
episode: a.seasonNumber
};
}) };
}
public async search(data: SearchData): Promise<SearchResponse> {
const funiSearch = await this.funi.searchShow(false, data);
if (!funiSearch.isOk)
return funiSearch;
return { isOk: true, value: funiSearch.value.items.hits.map(a => ({
image: a.image.showThumbnail,
name: a.title,
desc: a.description,
id: a.id,
lang: a.languages,
rating: a.starRating
})) };
}
public async checkToken(): Promise<CheckTokenResponse> {
return this.funi.checkToken();
}
public auth(data: AuthData) {
return this.funi.auth(data);
}
public async downloadItem(data: DownloadData) {
this.setDownloading(true);
const res = await this.funi.getShow(false, { all: false, but: false, id: parseInt(data.id), e: data.e });
const _default = buildDefault() as ArgvType;
if (!res.isOk)
return this.alertError(res.reason);
for (const ep of res.value) {
await this.funi.getEpisode(false, { dubLang: data.dubLang, fnSlug: ep, s: data.id, subs: { dlsubs: ['all'], sub: false } }, { ..._default, callbackMaker: this.makeProgressHandler.bind(this), ass: true, fileName: data.fileName, q: data.q });
}
this.sendMessage({ name: 'finish', data: undefined });
this.setDownloading(false);
}
}
export default FunimationHandler;

View file

@ -57,7 +57,7 @@ const MultiSelect: React.FC<MultiSelectProps> = (props) => {
)}
MenuProps={MenuProps}
>
{props.values.concat(props.allOption ? 'all' : []). map((name) => (
{props.values.concat(props.allOption ? 'all' : []).map((name) => (
<MenuItem
key={name}
value={name}

120
index.ts
View file

@ -1,61 +1,61 @@
import { ServiceClass } from './@types/serviceClassInterface';
import { appArgv, overrideArguments } from './modules/module.app-args';
import * as yamlCfg from './modules/module.cfg-loader';
import { makeCommand, addToArchive } from './modules/module.downloadArchive';
import update from './modules/module.updater';
(async () => {
const cfg = yamlCfg.loadCfg();
const argv = appArgv(cfg.cli);
await update(argv.update);
if (argv.all && argv.but) {
console.log('[ERROR] --all and --but exclude each other!');
return;
}
if (argv.addArchive) {
if (argv.service === 'funi') {
if (argv.s === undefined)
return console.log('[ERROR] `-s` not found');
addToArchive({
service: 'funi',
type: 's'
}, argv.s);
console.log('[INFO] Added %s to the downloadArchive list', argv.s);
} else if (argv.service === 'crunchy') {
if (argv.s === undefined && argv.series === undefined)
return console.log('[ERROR] `-s` or `--srz` not found');
if (argv.s && argv.series)
return console.log('[ERROR] Both `-s` and `--srz` found');
addToArchive({
service: 'crunchy',
type: argv.s === undefined ? 'srz' : 's'
}, (argv.s === undefined ? argv.series : argv.s) as string);
console.log('[INFO] Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
}
} else if (argv.downloadArchive) {
const ids = makeCommand(argv.service);
for (const id of ids) {
overrideArguments(cfg.cli, id);
/* Reimport module to override appArgv */
Object.keys(require.cache).forEach(key => {
if (key.endsWith('crunchy.js') || key.endsWith('funi.js'))
delete require.cache[key];
});
const service = new (argv.service === 'funi' ? (await import('./funi')).default : (await import('./crunchy')).default) as ServiceClass;
await service.cli();
}
} else {
if (argv.service === 'funi') {
const funi = new (await import('./funi')).default();
await funi.cli();
} else if (argv.service === 'crunchy') {
const crunchy = new (await import('./crunchy')).default();
await crunchy.cli();
}
}
import { ServiceClass } from './@types/serviceClassInterface';
import { appArgv, overrideArguments } from './modules/module.app-args';
import * as yamlCfg from './modules/module.cfg-loader';
import { makeCommand, addToArchive } from './modules/module.downloadArchive';
import update from './modules/module.updater';
(async () => {
const cfg = yamlCfg.loadCfg();
const argv = appArgv(cfg.cli);
await update(argv.update);
if (argv.all && argv.but) {
console.log('[ERROR] --all and --but exclude each other!');
return;
}
if (argv.addArchive) {
if (argv.service === 'funi') {
if (argv.s === undefined)
return console.log('[ERROR] `-s` not found');
addToArchive({
service: 'funi',
type: 's'
}, argv.s);
console.log('[INFO] Added %s to the downloadArchive list', argv.s);
} else if (argv.service === 'crunchy') {
if (argv.s === undefined && argv.series === undefined)
return console.log('[ERROR] `-s` or `--srz` not found');
if (argv.s && argv.series)
return console.log('[ERROR] Both `-s` and `--srz` found');
addToArchive({
service: 'crunchy',
type: argv.s === undefined ? 'srz' : 's'
}, (argv.s === undefined ? argv.series : argv.s) as string);
console.log('[INFO] Added %s to the downloadArchive list', (argv.s === undefined ? argv.series : argv.s));
}
} else if (argv.downloadArchive) {
const ids = makeCommand(argv.service);
for (const id of ids) {
overrideArguments(cfg.cli, id);
/* Reimport module to override appArgv */
Object.keys(require.cache).forEach(key => {
if (key.endsWith('crunchy.js') || key.endsWith('funi.js'))
delete require.cache[key];
});
const service = new (argv.service === 'funi' ? (await import('./funi')).default : (await import('./crunchy')).default) as ServiceClass;
await service.cli();
}
} else {
if (argv.service === 'funi') {
const funi = new (await import('./funi')).default();
await funi.cli();
} else if (argv.service === 'crunchy') {
const crunchy = new (await import('./crunchy')).default();
await crunchy.cli();
}
}
})();

View file

@ -1,63 +1,63 @@
import packageJSON from '../package.json';
import fs from 'fs';
import path from 'path';
import { args, groups } from './module.args';
const transformService = (str: 'funi'|'crunchy'|'both') => {
switch (str) {
case 'both':
return 'Both';
case 'crunchy':
return 'Crunchyroll';
case 'funi':
return 'Funimation';
}
};
let docs = `# ${packageJSON.name} (${packageJSON.version}v)
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](${packageJSON.bugs.url}).
## Legal Warning
This application is not endorsed by or affiliated with *Funimation* or *Crunchyroll*.
This application enables you to download videos for offline viewing which may be forbidden by law in your country.
The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider.
This tool is not responsible for your actions; please make an informed decision before using this application.
## CLI Options
### Legend
- \`\${someText}\` shows that you should replace this text with your own
- e.g. \`--username \${someText}\` -> \`--username Izuco\`
`;
Object.entries(groups).forEach(([key, value]) => {
docs += `\n### ${value.slice(0, -1)}\n`;
docs += args.filter(a => a.group === key).map(argument => {
return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``,
`| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`,
`| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `,
`| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|`
+ ` \`${(argument.alias ? `${argument.alias.length > 1 ? '--' : '-'}${argument.alias}` : undefined) ?? 'NaN'}\` |`
+ `${argument.choices ? ` [${argument.choices.map(a => `\`${a || '\'\''}\``).join(', ')}] |` : ''}`
+ `${argument.default ? ` \`${
typeof argument.default === 'object'
? Array.isArray(argument.default)
? JSON.stringify(argument.default)
: argument.default.default
: argument.default
}\`|` : ''}`
+ ` ${typeof argument.default === 'object' && !Array.isArray(argument.default)
? `\`${argument.default.name || argument.name}: \``
: '`NaN`'
} |`,
'',
argument.docDescribe === true ? argument.describe : argument.docDescribe
].join('\n');
}).join('\n');
});
import packageJSON from '../package.json';
import fs from 'fs';
import path from 'path';
import { args, groups } from './module.args';
const transformService = (str: 'funi'|'crunchy'|'both') => {
switch (str) {
case 'both':
return 'Both';
case 'crunchy':
return 'Crunchyroll';
case 'funi':
return 'Funimation';
}
};
let docs = `# ${packageJSON.name} (${packageJSON.version}v)
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](${packageJSON.bugs.url}).
## Legal Warning
This application is not endorsed by or affiliated with *Funimation* or *Crunchyroll*.
This application enables you to download videos for offline viewing which may be forbidden by law in your country.
The usage of this application may also cause a violation of the *Terms of Service* between you and the stream provider.
This tool is not responsible for your actions; please make an informed decision before using this application.
## CLI Options
### Legend
- \`\${someText}\` shows that you should replace this text with your own
- e.g. \`--username \${someText}\` -> \`--username Izuco\`
`;
Object.entries(groups).forEach(([key, value]) => {
docs += `\n### ${value.slice(0, -1)}\n`;
docs += args.filter(a => a.group === key).map(argument => {
return [`#### \`${argument.name.length > 1 ? '--' : '-'}${argument.name}\``,
`| **Service** | **Usage** | **Type** | **Required** | **Alias** | ${argument.choices ? '**Choices** |' : ''} ${argument.default ? '**Default** |' : ''}**cli-default Entry**`,
`| --- | --- | --- | --- | --- | ${argument.choices ? '--- | ' : ''}${argument.default ? '--- | ' : ''}---| `,
`| ${transformService(argument.service)} | \`${argument.name.length > 1 ? '--' : '-'}${argument.name} ${argument.usage}\` | \`${argument.type}\` | \`${argument.demandOption ? 'Yes' : 'No'}\`|`
+ ` \`${(argument.alias ? `${argument.alias.length > 1 ? '--' : '-'}${argument.alias}` : undefined) ?? 'NaN'}\` |`
+ `${argument.choices ? ` [${argument.choices.map(a => `\`${a || '\'\''}\``).join(', ')}] |` : ''}`
+ `${argument.default ? ` \`${
typeof argument.default === 'object'
? Array.isArray(argument.default)
? JSON.stringify(argument.default)
: argument.default.default
: argument.default
}\`|` : ''}`
+ ` ${typeof argument.default === 'object' && !Array.isArray(argument.default)
? `\`${argument.default.name || argument.name}: \``
: '`NaN`'
} |`,
'',
argument.docDescribe === true ? argument.describe : argument.docDescribe
].join('\n');
}).join('\n');
});
fs.writeFileSync(path.resolve(__dirname, '..', 'docs', 'DOCUMENTATION.md'), docs);

View file

@ -1,117 +1,117 @@
// build requirements
import fs from 'fs-extra';
import pkg from '../package.json';
import modulesCleanup from 'removeNPMAbsolutePaths';
import { exec } from 'pkg';
import { execSync } from 'child_process';
import path from 'path';
const buildsDir = './_builds';
const nodeVer = 'node16-';
type BuildTypes = `${'ubuntu'|'windows'|'macos'}64`
(async () => {
const buildType = process.argv[2] as BuildTypes;
const isGUI = process.argv[3] === 'true';
if (isGUI) {
buildGUI(buildType);
} else {
buildBinary(buildType);
}
})();
async function buildGUI(buildType: BuildTypes) {
execSync(`npx electron-builder build ${getCommand(buildType)}`, { stdio: [0,1,2] });
execSync(`7z a -t7z "../${buildsDir}/multi-downloader-nx-${buildType}-gui.7z" "${getOutputFileName(buildType)}"`,{
stdio:[0,1,2],
cwd: path.join('dist')
});
}
function getCommand(buildType: BuildTypes) {
switch (buildType) {
case 'ubuntu64':
return `--linux --arm64`
case 'windows64':
return '--win';
case 'macos64':
return '--mac dmg';
default:
return '--error'
}
}
function getOutputFileName(buildType: BuildTypes) {
switch (buildType) {
case 'ubuntu64':
return `${pkg.name}_${pkg.version}_arm64.deb`;
case 'windows64':
return `${pkg.name} Setup ${pkg.version}.exe`;
case 'macos64':
return `${pkg.name}-${pkg.version}.dmg`;
default:
throw new Error(`Unknown build type ${buildType}`);
}
}
// main
async function buildBinary(buildType: BuildTypes) {
const buildStr = `multi-downloader-nx`;
const acceptableBuilds = ['windows64','ubuntu64','macos64'];
if(!acceptableBuilds.includes(buildType)){
console.error('[ERROR] unknown build type!');
process.exit(1);
}
await modulesCleanup('.');
if(!fs.existsSync(buildsDir)){
fs.mkdirSync(buildsDir);
}
const buildFull = `${buildStr}-${buildType}-cli`;
const buildDir = `${buildsDir}/${buildFull}`;
if(fs.existsSync(buildDir)){
fs.removeSync(buildDir);
}
fs.mkdirSync(buildDir);
const buildConfig = [
pkg.main,
'--target', nodeVer + getTarget(buildType),
'--output', `${buildDir}/${pkg.short_name}`,
];
console.log(`[Build] Build configuration: ${buildFull}`);
try {
await exec(buildConfig);
}
catch(e){
console.log(e);
process.exit(1);
}
fs.mkdirSync(`${buildDir}/config`);
fs.mkdirSync(`${buildDir}/videos`);
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
fs.copySync('./package.json', `${buildDir}/package.json`);
fs.copySync('./docs/', `${buildDir}/docs/`);
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
}
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
};
function getTarget(bt: string) : string {
switch(bt){
case 'windows64':
return 'windows-x64';
case 'ubuntu64':
return 'linux-x64';
case 'macos64':
return 'macos-x64';
default:
return 'windows-x64';
}
}
// build requirements
import fs from 'fs-extra';
import pkg from '../package.json';
import modulesCleanup from 'removeNPMAbsolutePaths';
import { exec } from 'pkg';
import { execSync } from 'child_process';
import path from 'path';
const buildsDir = './_builds';
const nodeVer = 'node16-';
type BuildTypes = `${'ubuntu'|'windows'|'macos'}64`
(async () => {
const buildType = process.argv[2] as BuildTypes;
const isGUI = process.argv[3] === 'true';
if (isGUI) {
buildGUI(buildType);
} else {
buildBinary(buildType);
}
})();
async function buildGUI(buildType: BuildTypes) {
execSync(`npx electron-builder build ${getCommand(buildType)}`, { stdio: [0,1,2] });
execSync(`7z a -t7z "../${buildsDir}/multi-downloader-nx-${buildType}-gui.7z" "${getOutputFileName(buildType)}"`,{
stdio:[0,1,2],
cwd: path.join('dist')
});
}
function getCommand(buildType: BuildTypes) {
switch (buildType) {
case 'ubuntu64':
return '--linux --arm64';
case 'windows64':
return '--win';
case 'macos64':
return '--mac dmg';
default:
return '--error';
}
}
function getOutputFileName(buildType: BuildTypes) {
switch (buildType) {
case 'ubuntu64':
return `${pkg.name}_${pkg.version}_arm64.deb`;
case 'windows64':
return `${pkg.name} Setup ${pkg.version}.exe`;
case 'macos64':
return `${pkg.name}-${pkg.version}.dmg`;
default:
throw new Error(`Unknown build type ${buildType}`);
}
}
// main
async function buildBinary(buildType: BuildTypes) {
const buildStr = 'multi-downloader-nx';
const acceptableBuilds = ['windows64','ubuntu64','macos64'];
if(!acceptableBuilds.includes(buildType)){
console.error('[ERROR] unknown build type!');
process.exit(1);
}
await modulesCleanup('.');
if(!fs.existsSync(buildsDir)){
fs.mkdirSync(buildsDir);
}
const buildFull = `${buildStr}-${buildType}-cli`;
const buildDir = `${buildsDir}/${buildFull}`;
if(fs.existsSync(buildDir)){
fs.removeSync(buildDir);
}
fs.mkdirSync(buildDir);
const buildConfig = [
pkg.main,
'--target', nodeVer + getTarget(buildType),
'--output', `${buildDir}/${pkg.short_name}`,
];
console.log(`[Build] Build configuration: ${buildFull}`);
try {
await exec(buildConfig);
}
catch(e){
console.log(e);
process.exit(1);
}
fs.mkdirSync(`${buildDir}/config`);
fs.mkdirSync(`${buildDir}/videos`);
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
fs.copySync('./config/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
fs.copySync('./modules/cmd-here.bat', `${buildDir}/cmd-here.bat`);
fs.copySync('./modules/NotoSans-Regular.ttf', `${buildDir}/NotoSans-Regular.ttf`);
fs.copySync('./package.json', `${buildDir}/package.json`);
fs.copySync('./docs/', `${buildDir}/docs/`);
fs.copySync('./LICENSE.md', `${buildDir}/docs/LICENSE.md`);
if(fs.existsSync(`${buildsDir}/${buildFull}.7z`)){
fs.removeSync(`${buildsDir}/${buildFull}.7z`);
}
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
}
function getTarget(bt: string) : string {
switch(bt){
case 'windows64':
return 'windows-x64';
case 'ubuntu64':
return 'linux-x64';
case 'macos64':
return 'macos-x64';
default:
return 'windows-x64';
}
}

View file

@ -1,75 +1,75 @@
import { Headers } from 'got/dist/source';
// api domains
const domain = {
www: 'https://www.crunchyroll.com',
api: 'https://api.crunchyroll.com',
www_beta: 'https://beta.crunchyroll.com',
api_beta: 'https://beta-api.crunchyroll.com',
};
export type APIType = {
newani: string,
search1: string,
search2: string,
rss_cid: string,
rss_gid: string
media_page: string
series_page: string
auth: string
// mobile api
search3: string
session: string
collections: string
// beta api
beta_auth: string
beta_authBasic: string
beta_authBasicMob: string
beta_profile: string
beta_cmsToken: string
beta_search: string
beta_browse: string
beta_cms: string,
beta_authHeader: Headers,
beta_authHeaderMob: Headers
}
// api urls
const api: APIType = {
// web
newani: `${domain.www}/rss/anime`,
search1: `${domain.www}/ajax/?req=RpcApiSearch_GetSearchCandidates`,
search2: `${domain.www}/search_page`,
rss_cid: `${domain.www}/syndication/feed?type=episodes&id=`, // &lang=enUS
rss_gid: `${domain.www}/syndication/feed?type=episodes&group_id=`, // &lang=enUS
media_page: `${domain.www}/media-`,
series_page: `${domain.www}/series-`,
auth: `${domain.www}/login`,
// mobile api
search3: `${domain.api}/autocomplete.0.json`,
session: `${domain.api}/start_session.0.json`,
collections: `${domain.api}/list_collections.0.json`,
// beta api
beta_auth: `${domain.api_beta}/auth/v1/token`,
beta_authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
beta_authBasicMob: 'Basic YTZ5eGxvYW04c2VqaThsZDhldnc6aFQ3d2FjWHhNaURJcDhSNE9kekJybWVoQUtLTEVKUEE=',
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
beta_cmsToken: `${domain.api_beta}/index/v2`,
beta_search: `${domain.api_beta}/content/v1/search`,
beta_browse: `${domain.api_beta}/content/v1/browse`,
beta_cms: `${domain.api_beta}/cms/v2`,
beta_authHeader: {},
beta_authHeaderMob: {}
};
// set header
api.beta_authHeader = {
Authorization: api.beta_authBasic,
};
api.beta_authHeaderMob = {
Authorization: api.beta_authBasicMob,
};
export {
domain, api
};
import { Headers } from 'got/dist/source';
// api domains
const domain = {
www: 'https://www.crunchyroll.com',
api: 'https://api.crunchyroll.com',
www_beta: 'https://beta.crunchyroll.com',
api_beta: 'https://beta-api.crunchyroll.com',
};
export type APIType = {
newani: string,
search1: string,
search2: string,
rss_cid: string,
rss_gid: string
media_page: string
series_page: string
auth: string
// mobile api
search3: string
session: string
collections: string
// beta api
beta_auth: string
beta_authBasic: string
beta_authBasicMob: string
beta_profile: string
beta_cmsToken: string
beta_search: string
beta_browse: string
beta_cms: string,
beta_authHeader: Headers,
beta_authHeaderMob: Headers
}
// api urls
const api: APIType = {
// web
newani: `${domain.www}/rss/anime`,
search1: `${domain.www}/ajax/?req=RpcApiSearch_GetSearchCandidates`,
search2: `${domain.www}/search_page`,
rss_cid: `${domain.www}/syndication/feed?type=episodes&id=`, // &lang=enUS
rss_gid: `${domain.www}/syndication/feed?type=episodes&group_id=`, // &lang=enUS
media_page: `${domain.www}/media-`,
series_page: `${domain.www}/series-`,
auth: `${domain.www}/login`,
// mobile api
search3: `${domain.api}/autocomplete.0.json`,
session: `${domain.api}/start_session.0.json`,
collections: `${domain.api}/list_collections.0.json`,
// beta api
beta_auth: `${domain.api_beta}/auth/v1/token`,
beta_authBasic: 'Basic bm9haWhkZXZtXzZpeWcwYThsMHE6',
beta_authBasicMob: 'Basic YTZ5eGxvYW04c2VqaThsZDhldnc6aFQ3d2FjWHhNaURJcDhSNE9kekJybWVoQUtLTEVKUEE=',
beta_profile: `${domain.api_beta}/accounts/v1/me/profile`,
beta_cmsToken: `${domain.api_beta}/index/v2`,
beta_search: `${domain.api_beta}/content/v1/search`,
beta_browse: `${domain.api_beta}/content/v1/browse`,
beta_cms: `${domain.api_beta}/cms/v2`,
beta_authHeader: {},
beta_authHeaderMob: {}
};
// set header
api.beta_authHeader = {
Authorization: api.beta_authBasic,
};
api.beta_authHeaderMob = {
Authorization: api.beta_authBasicMob,
};
export {
domain, api
};

View file

@ -1,67 +1,67 @@
import yargs, { Choices } from 'yargs';
import { args, AvailableMuxer, groups } from './module.args';
let argvC: { [x: string]: unknown; fsRetryTime: number, forceMuxer: AvailableMuxer|undefined; username: string|undefined, password: string|undefined, silentAuth: boolean, skipSubMux: boolean, downloadArchive: boolean, addArchive: boolean, but: boolean, auth: boolean | undefined; dlFonts: boolean | undefined; search: string | undefined; 'search-type': string; page: number | undefined; 'search-locale': string; new: boolean | undefined; 'movie-listing': string | undefined; series: string | undefined; s: string | undefined; e: string | undefined; q: number; x: number; kstream: number; partsize: number; hslang: string; dlsubs: string[]; novids: boolean | undefined; noaudio: boolean | undefined; nosubs: boolean | undefined; dubLang: string[]; all: boolean; fontSize: number; allDubs: boolean; timeout: number; simul: boolean; mp4: boolean; skipmux: boolean | undefined; fileName: string; numbers: number; nosess: string; debug: boolean | undefined; nocleanup: boolean; help: boolean | undefined; service: 'funi' | 'crunchy'; update: boolean; fontName: string | undefined; _: (string | number)[]; $0: string; };
export type ArgvType = typeof argvC;
const appArgv = (cfg: {
[key: string]: unknown
}) => {
if (argvC)
return argvC;
yargs(process.argv.slice(2));
const argv = getArgv(cfg)
.parseSync();
argvC = argv;
return argv;
};
const overrideArguments = (cfg: { [key:string]: unknown }, override: Partial<typeof argvC>) => {
const argv = getArgv(cfg).middleware((ar) => {
for (const key of Object.keys(override)) {
ar[key] = override[key];
}
return ar;
}).parseSync();
argvC = argv;
};
export {
appArgv,
overrideArguments
};
const getArgv = (cfg: { [key:string]: unknown }) => {
const parseDefault = <T = unknown>(key: string, _default: T) : T=> {
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
return cfg[key] as T;
} else
return _default;
};
const argv = yargs.parserConfiguration({
'duplicate-arguments-array': false,
'camel-case-expansion': false,
})
.wrap(yargs.terminalWidth())
.usage('Usage: $0 [options]')
.help(true).version(false);
const data = args.map(a => {
return {
...a,
group: groups[a.group],
default: typeof a.default === 'object' && !Array.isArray(a.default) ?
parseDefault(a.default.name || a.name, a.default.default) : a.default
};
});
for (const item of data)
argv.option(item.name, {
...item,
choices: item.choices as unknown as Choices
});
return argv as unknown as yargs.Argv<typeof argvC>;
};
import yargs, { Choices } from 'yargs';
import { args, AvailableMuxer, groups } from './module.args';
let argvC: { [x: string]: unknown; fsRetryTime: number, forceMuxer: AvailableMuxer|undefined; username: string|undefined, password: string|undefined, silentAuth: boolean, skipSubMux: boolean, downloadArchive: boolean, addArchive: boolean, but: boolean, auth: boolean | undefined; dlFonts: boolean | undefined; search: string | undefined; 'search-type': string; page: number | undefined; 'search-locale': string; new: boolean | undefined; 'movie-listing': string | undefined; series: string | undefined; s: string | undefined; e: string | undefined; q: number; x: number; kstream: number; partsize: number; hslang: string; dlsubs: string[]; novids: boolean | undefined; noaudio: boolean | undefined; nosubs: boolean | undefined; dubLang: string[]; all: boolean; fontSize: number; allDubs: boolean; timeout: number; simul: boolean; mp4: boolean; skipmux: boolean | undefined; fileName: string; numbers: number; nosess: string; debug: boolean | undefined; nocleanup: boolean; help: boolean | undefined; service: 'funi' | 'crunchy'; update: boolean; fontName: string | undefined; _: (string | number)[]; $0: string; };
export type ArgvType = typeof argvC;
const appArgv = (cfg: {
[key: string]: unknown
}) => {
if (argvC)
return argvC;
yargs(process.argv.slice(2));
const argv = getArgv(cfg)
.parseSync();
argvC = argv;
return argv;
};
const overrideArguments = (cfg: { [key:string]: unknown }, override: Partial<typeof argvC>) => {
const argv = getArgv(cfg).middleware((ar) => {
for (const key of Object.keys(override)) {
ar[key] = override[key];
}
return ar;
}).parseSync();
argvC = argv;
};
export {
appArgv,
overrideArguments
};
const getArgv = (cfg: { [key:string]: unknown }) => {
const parseDefault = <T = unknown>(key: string, _default: T) : T=> {
if (Object.prototype.hasOwnProperty.call(cfg, key)) {
return cfg[key] as T;
} else
return _default;
};
const argv = yargs.parserConfiguration({
'duplicate-arguments-array': false,
'camel-case-expansion': false,
})
.wrap(yargs.terminalWidth())
.usage('Usage: $0 [options]')
.help(true).version(false);
const data = args.map(a => {
return {
...a,
group: groups[a.group],
default: typeof a.default === 'object' && !Array.isArray(a.default) ?
parseDefault(a.default.name || a.name, a.default.default) : a.default
};
});
for (const item of data)
argv.option(item.name, {
...item,
choices: item.choices as unknown as Choices
});
return argv as unknown as yargs.Argv<typeof argvC>;
};

File diff suppressed because it is too large Load diff

View file

@ -1,208 +1,208 @@
import path from 'path';
import yaml from 'yaml';
import fs from 'fs-extra';
import { lookpath } from 'lookpath';
// new-cfg
const workingDir = (process as NodeJS.Process & {
pkg?: unknown
}).pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
const binCfgFile = path.join(workingDir, 'config', 'bin-path');
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
const sessCfgFile = path.join(workingDir, 'config', 'session');
const tokenFile = {
funi: path.join(workingDir, 'config', 'funi_token'),
cr: path.join(workingDir, 'config', 'cr_token')
};
const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: boolean): T => {
if(fs.existsSync(`${file}.user.yml`) && !isSess){
file += '.user';
}
file += '.yml';
if(fs.existsSync(file)){
try{
return yaml.parse(fs.readFileSync(file, 'utf8'));
}
catch(e){
console.log('[ERROR]', e);
return {} as T;
}
}
return {} as T;
};
export type ConfigObject = {
dir: {
content: string,
trash: string,
fonts: string;
},
bin: {
ffmpeg?: string,
mkvmerge?: string
},
cli: {
[key: string]: any
}
}
const loadCfg = () : ConfigObject => {
// load cfgs
const defaultCfg: ConfigObject = {
bin: {},
dir: loadYamlCfgFile<{
content: string,
trash: string,
fonts: string
}>(dirCfgFile),
cli: loadYamlCfgFile<{
[key: string]: any
}>(cliCfgFile),
};
const defaultDirs = {
fonts: '${wdir}/fonts/',
content: '${wdir}/videos/',
trash: '${wdir}/videos/_trash/',
};
if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) {
defaultCfg.dir = defaultDirs;
}
const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[];
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') {
defaultCfg.dir[key] = defaultDirs[key];
}
if (!path.isAbsolute(defaultCfg.dir[key])) {
defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, ''));
}
}
if(!fs.existsSync(defaultCfg.dir.content)){
try{
fs.ensureDirSync(defaultCfg.dir.content);
}
catch(e){
console.log('[ERROR] Content directory not accessible!');
return defaultCfg;
}
}
if(!fs.existsSync(defaultCfg.dir.trash)){
defaultCfg.dir.trash = defaultCfg.dir.content;
}
// output
return defaultCfg;
};
const loadBinCfg = async () => {
const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile);
// binaries
const defaultBin = {
ffmpeg: '${wdir}/bin/ffmpeg/ffmpeg',
mkvmerge: '${wdir}/bin/mkvtoolnix/mkvmerge',
};
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
for(const dir of keys){
if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){
binCfg[dir] = defaultBin[dir];
}
if (!path.isAbsolute(binCfg[dir] as string) && (binCfg[dir] as string).match(/^\${wdir}/)){
binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, '');
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
}
binCfg[dir] = await lookpath(binCfg[dir] as string);
binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined;
if(!binCfg[dir]){
const binFile = await lookpath(path.basename(defaultBin[dir]));
binCfg[dir] = binFile ? binFile : binCfg[dir];
}
}
return binCfg;
};
const loadCRSession = () => {
let session = loadYamlCfgFile(sessCfgFile, true);
if(typeof session !== 'object' || session === null || Array.isArray(session)){
session = {};
}
for(const cv of Object.keys(session)){
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
session[cv] = {};
}
}
return session;
};
const saveCRSession = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(sessCfgFile);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${sessCfgFile}.yml`, yaml.stringify(data));
}
catch(e){
console.log('[ERROR] Can\'t save session file to disk!');
}
};
const loadCRToken = () => {
let token = loadYamlCfgFile(tokenFile.cr, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
};
const saveCRToken = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(tokenFile.cr);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data));
}
catch(e){
console.log('[ERROR] Can\'t save token file to disk!');
}
};
const loadFuniToken = () => {
const loadedToken = loadYamlCfgFile<{
token?: string
}>(tokenFile.funi, true);
let token: false|string = false;
if (loadedToken && loadedToken.token)
token = loadedToken.token;
// info if token not set
if(!token){
console.log('[INFO] Token not set!\n');
}
return token;
};
const saveFuniToken = (data: {
token?: string
}) => {
const cfgFolder = path.dirname(tokenFile.funi);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.funi}.yml`, yaml.stringify(data));
}
catch(e){
console.log('[ERROR] Can\'t save token file to disk!');
}
};
const cfgDir = path.join(workingDir, 'config');
export {
loadBinCfg,
loadCfg,
loadFuniToken,
saveFuniToken,
saveCRSession,
saveCRToken,
loadCRToken,
loadCRSession,
sessCfgFile,
cfgDir
import path from 'path';
import yaml from 'yaml';
import fs from 'fs-extra';
import { lookpath } from 'lookpath';
// new-cfg
const workingDir = (process as NodeJS.Process & {
pkg?: unknown
}).pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
const binCfgFile = path.join(workingDir, 'config', 'bin-path');
const dirCfgFile = path.join(workingDir, 'config', 'dir-path');
const cliCfgFile = path.join(workingDir, 'config', 'cli-defaults');
const sessCfgFile = path.join(workingDir, 'config', 'session');
const tokenFile = {
funi: path.join(workingDir, 'config', 'funi_token'),
cr: path.join(workingDir, 'config', 'cr_token')
};
const loadYamlCfgFile = <T extends Record<string, any>>(file: string, isSess?: boolean): T => {
if(fs.existsSync(`${file}.user.yml`) && !isSess){
file += '.user';
}
file += '.yml';
if(fs.existsSync(file)){
try{
return yaml.parse(fs.readFileSync(file, 'utf8'));
}
catch(e){
console.log('[ERROR]', e);
return {} as T;
}
}
return {} as T;
};
export type ConfigObject = {
dir: {
content: string,
trash: string,
fonts: string;
},
bin: {
ffmpeg?: string,
mkvmerge?: string
},
cli: {
[key: string]: any
}
}
const loadCfg = () : ConfigObject => {
// load cfgs
const defaultCfg: ConfigObject = {
bin: {},
dir: loadYamlCfgFile<{
content: string,
trash: string,
fonts: string
}>(dirCfgFile),
cli: loadYamlCfgFile<{
[key: string]: any
}>(cliCfgFile),
};
const defaultDirs = {
fonts: '${wdir}/fonts/',
content: '${wdir}/videos/',
trash: '${wdir}/videos/_trash/',
};
if (typeof defaultCfg.dir !== 'object' || defaultCfg.dir === null || Array.isArray(defaultCfg.dir)) {
defaultCfg.dir = defaultDirs;
}
const keys = Object.keys(defaultDirs) as (keyof typeof defaultDirs)[];
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(defaultCfg.dir, key) || typeof defaultCfg.dir[key] !== 'string') {
defaultCfg.dir[key] = defaultDirs[key];
}
if (!path.isAbsolute(defaultCfg.dir[key])) {
defaultCfg.dir[key] = path.join(workingDir, defaultCfg.dir[key].replace(/^\${wdir}/, ''));
}
}
if(!fs.existsSync(defaultCfg.dir.content)){
try{
fs.ensureDirSync(defaultCfg.dir.content);
}
catch(e){
console.log('[ERROR] Content directory not accessible!');
return defaultCfg;
}
}
if(!fs.existsSync(defaultCfg.dir.trash)){
defaultCfg.dir.trash = defaultCfg.dir.content;
}
// output
return defaultCfg;
};
const loadBinCfg = async () => {
const binCfg = loadYamlCfgFile<ConfigObject['bin']>(binCfgFile);
// binaries
const defaultBin = {
ffmpeg: '${wdir}/bin/ffmpeg/ffmpeg',
mkvmerge: '${wdir}/bin/mkvtoolnix/mkvmerge',
};
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
for(const dir of keys){
if(!Object.prototype.hasOwnProperty.call(binCfg, dir) || typeof binCfg[dir] != 'string'){
binCfg[dir] = defaultBin[dir];
}
if (!path.isAbsolute(binCfg[dir] as string) && (binCfg[dir] as string).match(/^\${wdir}/)){
binCfg[dir] = (binCfg[dir] as string).replace(/^\${wdir}/, '');
binCfg[dir] = path.join(workingDir, binCfg[dir] as string);
}
binCfg[dir] = await lookpath(binCfg[dir] as string);
binCfg[dir] = binCfg[dir] ? binCfg[dir] : undefined;
if(!binCfg[dir]){
const binFile = await lookpath(path.basename(defaultBin[dir]));
binCfg[dir] = binFile ? binFile : binCfg[dir];
}
}
return binCfg;
};
const loadCRSession = () => {
let session = loadYamlCfgFile(sessCfgFile, true);
if(typeof session !== 'object' || session === null || Array.isArray(session)){
session = {};
}
for(const cv of Object.keys(session)){
if(typeof session[cv] !== 'object' || session[cv] === null || Array.isArray(session[cv])){
session[cv] = {};
}
}
return session;
};
const saveCRSession = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(sessCfgFile);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${sessCfgFile}.yml`, yaml.stringify(data));
}
catch(e){
console.log('[ERROR] Can\'t save session file to disk!');
}
};
const loadCRToken = () => {
let token = loadYamlCfgFile(tokenFile.cr, true);
if(typeof token !== 'object' || token === null || Array.isArray(token)){
token = {};
}
return token;
};
const saveCRToken = (data: Record<string, unknown>) => {
const cfgFolder = path.dirname(tokenFile.cr);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.cr}.yml`, yaml.stringify(data));
}
catch(e){
console.log('[ERROR] Can\'t save token file to disk!');
}
};
const loadFuniToken = () => {
const loadedToken = loadYamlCfgFile<{
token?: string
}>(tokenFile.funi, true);
let token: false|string = false;
if (loadedToken && loadedToken.token)
token = loadedToken.token;
// info if token not set
if(!token){
console.log('[INFO] Token not set!\n');
}
return token;
};
const saveFuniToken = (data: {
token?: string
}) => {
const cfgFolder = path.dirname(tokenFile.funi);
try{
fs.ensureDirSync(cfgFolder);
fs.writeFileSync(`${tokenFile.funi}.yml`, yaml.stringify(data));
}
catch(e){
console.log('[ERROR] Can\'t save token file to disk!');
}
};
const cfgDir = path.join(workingDir, 'config');
export {
loadBinCfg,
loadCfg,
loadFuniToken,
saveFuniToken,
saveCRSession,
saveCRToken,
loadCRToken,
loadCRSession,
sessCfgFile,
cfgDir
};

View file

@ -1,26 +1,26 @@
const parse = (data: string) => {
const res: Record<string, {
value: string,
expires: Date,
path: string,
domain: string,
secure: boolean
}> = {};
const split = data.replace(/\r/g,'').split('\n');
for (const line of split) {
const c = line.split('\t');
if(c.length < 7){
continue;
}
res[c[5]] = {
value: c[6],
expires: new Date(parseInt(c[4])*1000),
path: c[2],
domain: c[0].replace(/^\./,''),
secure: c[3] == 'TRUE' ? true : false
};
}
return res;
};
export default parse;
const parse = (data: string) => {
const res: Record<string, {
value: string,
expires: Date,
path: string,
domain: string,
secure: boolean
}> = {};
const split = data.replace(/\r/g,'').split('\n');
for (const line of split) {
const c = line.split('\t');
if(c.length < 7){
continue;
}
res[c[5]] = {
value: c[6],
expires: new Date(parseInt(c[4])*1000),
path: c[2],
domain: c[0].replace(/^\./,''),
secure: c[3] == 'TRUE' ? true : false
};
}
return res;
};
export default parse;

View file

@ -1,162 +1,162 @@
// 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?: Headers,
curlProxy?: boolean,
curlProxyAuth?: string,
minVersion?: string,
http2?: boolean,
body?: unknown,
curlDebug?: boolean
} | undefined;
export type Res = {
httpVersion: string,
statusCode: string,
statusMessage: string,
rawHeaders: string,
headers: Record<string, string[]|string>,
rawBody: Buffer,
body: string,
}
// req
const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache: string) => {
const curlOpt = [
`"${curlBin}"`,
`"${url}"`,
];
options = options || {};
if(options.headers && Object.keys(options.headers).length > 0){
for(const h of Object.keys(options.headers)){
const hC = options.headers[h];
curlOpt.push('-H', `"${h}: ${hC}"`);
}
}
if(options.curlProxy){
curlOpt.push('--proxy-insecure', '-x', `"${options.curlProxy}"`);
if(options.curlProxyAuth && typeof options.curlProxyAuth == 'string' && options.curlProxyAuth.match(':')){
curlOpt.push('-U', `"${options.curlProxyAuth}"`);
}
}
const reqId = uuidv4();
const headFile = path.join(cache, `/res-headers-${reqId}`);
const bodyFile = path.join(cache, `/res-body-${reqId}`);
const errFile = path.join(cache, `/res-err-${reqId}`);
curlOpt.push('-D', `"${headFile}"`);
curlOpt.push('-o', `"${bodyFile}"`);
curlOpt.push('--stderr', `"${errFile}"`);
curlOpt.push('-L', '-s', '-S');
if(options.minVersion == 'TLSv1.3'){
curlOpt.push('--tlsv1.3');
}
if(options.http2){
curlOpt.push('--http2');
}
if(options.body){
curlOpt.push('--data-urlencode', `"${options.body}"`);
}
const curlComm = curlOpt.join(' ');
try{
if(options.curlDebug){
console.log(curlComm, '\n');
}
child_process.execSync(curlComm, { stdio: 'inherit', windowsHide: true });
}
catch(next){
const errData = { name: 'RequestError', message: 'EACCES' };
try{
fs.unlinkSync(headFile);
}
catch(e){
// ignore it...
}
try{
errData.message =
fs.readFileSync(errFile, 'utf8')
.replace(/^curl: /, '');
fs.unlinkSync(errFile);
}
catch(e){
// ignore it...
}
throw errData;
}
const rawHeaders = fs.readFileSync(headFile, 'utf8');
const rawBody = fs.readFileSync(bodyFile);
fs.unlinkSync(headFile);
fs.unlinkSync(bodyFile);
fs.unlinkSync(errFile);
const res: Res = {
httpVersion: '',
statusCode: '',
statusMessage: '',
rawHeaders: rawHeaders,
headers: {},
rawBody: rawBody,
body: rawBody.toString(),
};
const headersCont = rawHeaders.replace(/\r/g, '').split('\n');
for(const h of headersCont){
if( h == '' ){ continue; }
if(!h.match(':')){
const statusRes = h.split(' ');
res.httpVersion = statusRes[0].split('/')[1];
res.statusCode = statusRes[1];
res.statusMessage = statusRes.slice(2).join(' ');
}
else{
const resHeader = h.split(': ');
const resHeadName = resHeader[0].toLowerCase();
const resHeadCont = resHeader.slice(1).join(': ');
if(resHeadName == 'set-cookie'){
if(!Object.prototype.hasOwnProperty.call(res.headers, resHeadName)){
res.headers[resHeadName] = [];
}
(res.headers[resHeadName] as string[]).push(resHeadCont);
}
else{
res.headers[resHeadName] = resHeadCont;
}
}
}
if(!res.statusCode.match(/^(2|3)\d\d$/)){
const httpStatusMessage = res.statusMessage ? ` (${res.statusMessage})` : '';
throw {
name: 'HTTPError',
message: `Response code ${res.statusCode}${httpStatusMessage}`,
response: res
};
}
return res;
};
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
export default curlReq;
// 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?: Headers,
curlProxy?: boolean,
curlProxyAuth?: string,
minVersion?: string,
http2?: boolean,
body?: unknown,
curlDebug?: boolean
} | undefined;
export type Res = {
httpVersion: string,
statusCode: string,
statusMessage: string,
rawHeaders: string,
headers: Record<string, string[]|string>,
rawBody: Buffer,
body: string,
}
// req
const curlReq = async (curlBin: string, url: string, options: CurlOptions, cache: string) => {
const curlOpt = [
`"${curlBin}"`,
`"${url}"`,
];
options = options || {};
if(options.headers && Object.keys(options.headers).length > 0){
for(const h of Object.keys(options.headers)){
const hC = options.headers[h];
curlOpt.push('-H', `"${h}: ${hC}"`);
}
}
if(options.curlProxy){
curlOpt.push('--proxy-insecure', '-x', `"${options.curlProxy}"`);
if(options.curlProxyAuth && typeof options.curlProxyAuth == 'string' && options.curlProxyAuth.match(':')){
curlOpt.push('-U', `"${options.curlProxyAuth}"`);
}
}
const reqId = uuidv4();
const headFile = path.join(cache, `/res-headers-${reqId}`);
const bodyFile = path.join(cache, `/res-body-${reqId}`);
const errFile = path.join(cache, `/res-err-${reqId}`);
curlOpt.push('-D', `"${headFile}"`);
curlOpt.push('-o', `"${bodyFile}"`);
curlOpt.push('--stderr', `"${errFile}"`);
curlOpt.push('-L', '-s', '-S');
if(options.minVersion == 'TLSv1.3'){
curlOpt.push('--tlsv1.3');
}
if(options.http2){
curlOpt.push('--http2');
}
if(options.body){
curlOpt.push('--data-urlencode', `"${options.body}"`);
}
const curlComm = curlOpt.join(' ');
try{
if(options.curlDebug){
console.log(curlComm, '\n');
}
child_process.execSync(curlComm, { stdio: 'inherit', windowsHide: true });
}
catch(next){
const errData = { name: 'RequestError', message: 'EACCES' };
try{
fs.unlinkSync(headFile);
}
catch(e){
// ignore it...
}
try{
errData.message =
fs.readFileSync(errFile, 'utf8')
.replace(/^curl: /, '');
fs.unlinkSync(errFile);
}
catch(e){
// ignore it...
}
throw errData;
}
const rawHeaders = fs.readFileSync(headFile, 'utf8');
const rawBody = fs.readFileSync(bodyFile);
fs.unlinkSync(headFile);
fs.unlinkSync(bodyFile);
fs.unlinkSync(errFile);
const res: Res = {
httpVersion: '',
statusCode: '',
statusMessage: '',
rawHeaders: rawHeaders,
headers: {},
rawBody: rawBody,
body: rawBody.toString(),
};
const headersCont = rawHeaders.replace(/\r/g, '').split('\n');
for(const h of headersCont){
if( h == '' ){ continue; }
if(!h.match(':')){
const statusRes = h.split(' ');
res.httpVersion = statusRes[0].split('/')[1];
res.statusCode = statusRes[1];
res.statusMessage = statusRes.slice(2).join(' ');
}
else{
const resHeader = h.split(': ');
const resHeadName = resHeader[0].toLowerCase();
const resHeadCont = resHeader.slice(1).join(': ');
if(resHeadName == 'set-cookie'){
if(!Object.prototype.hasOwnProperty.call(res.headers, resHeadName)){
res.headers[resHeadName] = [];
}
(res.headers[resHeadName] as string[]).push(resHeadCont);
}
else{
res.headers[resHeadName] = resHeadCont;
}
}
}
if(!res.statusCode.match(/^(2|3)\d\d$/)){
const httpStatusMessage = res.statusMessage ? ` (${res.statusMessage})` : '';
throw {
name: 'HTTPError',
message: `Response code ${res.statusCode}${httpStatusMessage}`,
response: res
};
}
return res;
};
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
export default curlReq;

View file

@ -1,113 +1,113 @@
import * as path from 'path';
import * as fs from 'fs';
import { ArgvType } from './module.app-args';
const workingDir = (process as NodeJS.Process & {
pkg?: unknown
}).pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
export const archiveFile = path.join(workingDir, 'config', 'archive.json');
export type ItemType = {
id: string,
already: string[]
}[]
export type DataType = {
funi: {
s: ItemType
},
crunchy: {
srz: ItemType,
s: ItemType
}
}
const addToArchive = (kind: {
service: 'funi',
type: 's'
} | {
service: 'crunchy',
type: 's'|'srz'
}, ID: string) => {
const data = loadData();
if (Object.prototype.hasOwnProperty.call(data, kind.service)) {
const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type];
if (items.findIndex(a => a.id === ID) < 0) // Prevent duplicate
return;
items.push({
id: ID,
already: []
});
} else {
if (kind.service === 'funi') {
data['funi'] = {
s: [
{
id: ID,
already: []
}
]
};
} else {
data['crunchy'] = {
s: ([] as ItemType).concat(kind.type === 's' ? {
id: ID,
already: [] as string[]
} : []),
srz: ([] as ItemType).concat(kind.type === 'srz' ? {
id: ID,
already: [] as string[]
} : []),
};
}
}
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
};
const downloaded = (kind: {
service: 'funi',
type: 's'
} | {
service: 'crunchy',
type: 's'|'srz'
}, ID: string, episode: string[]) => {
let data = loadData();
if (!Object.prototype.hasOwnProperty.call(data, kind.service)) {
addToArchive(kind, ID);
data = loadData(); // Load updated version
}
(kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]).find(a => a.id === ID)?.already.push(...episode);
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
};
const makeCommand = (service: 'funi'|'crunchy') : Partial<ArgvType>[] => {
const data = loadData();
const ret: Partial<ArgvType>[] = [];
const kind = data[service];
for (const type of Object.keys(kind)) {
const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler
item.forEach(i => ret.push({
but: true,
all: false,
service,
e: i.already.join(','),
...(type === 's' ? {
s: i.id,
series: undefined
} : {
series: i.id,
s: undefined
})
}));
}
return ret;
};
const loadData = () : DataType => {
if (fs.existsSync(archiveFile))
return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType;
return {} as DataType;
};
import * as path from 'path';
import * as fs from 'fs';
import { ArgvType } from './module.app-args';
const workingDir = (process as NodeJS.Process & {
pkg?: unknown
}).pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
export const archiveFile = path.join(workingDir, 'config', 'archive.json');
export type ItemType = {
id: string,
already: string[]
}[]
export type DataType = {
funi: {
s: ItemType
},
crunchy: {
srz: ItemType,
s: ItemType
}
}
const addToArchive = (kind: {
service: 'funi',
type: 's'
} | {
service: 'crunchy',
type: 's'|'srz'
}, ID: string) => {
const data = loadData();
if (Object.prototype.hasOwnProperty.call(data, kind.service)) {
const items = kind.service === 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type];
if (items.findIndex(a => a.id === ID) < 0) // Prevent duplicate
return;
items.push({
id: ID,
already: []
});
} else {
if (kind.service === 'funi') {
data['funi'] = {
s: [
{
id: ID,
already: []
}
]
};
} else {
data['crunchy'] = {
s: ([] as ItemType).concat(kind.type === 's' ? {
id: ID,
already: [] as string[]
} : []),
srz: ([] as ItemType).concat(kind.type === 'srz' ? {
id: ID,
already: [] as string[]
} : []),
};
}
}
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
};
const downloaded = (kind: {
service: 'funi',
type: 's'
} | {
service: 'crunchy',
type: 's'|'srz'
}, ID: string, episode: string[]) => {
let data = loadData();
if (!Object.prototype.hasOwnProperty.call(data, kind.service)) {
addToArchive(kind, ID);
data = loadData(); // Load updated version
}
(kind.service == 'crunchy' ? data[kind.service][kind.type] : data[kind.service][kind.type]).find(a => a.id === ID)?.already.push(...episode);
fs.writeFileSync(archiveFile, JSON.stringify(data, null, 4));
};
const makeCommand = (service: 'funi'|'crunchy') : Partial<ArgvType>[] => {
const data = loadData();
const ret: Partial<ArgvType>[] = [];
const kind = data[service];
for (const type of Object.keys(kind)) {
const item = kind[type as 's']; // 'srz' is also possible but will be ignored for the compiler
item.forEach(i => ret.push({
but: true,
all: false,
service,
e: i.already.join(','),
...(type === 's' ? {
s: i.id,
series: undefined
} : {
series: i.id,
s: undefined
})
}));
}
return ret;
};
const loadData = () : DataType => {
if (fs.existsSync(archiveFile))
return JSON.parse(fs.readFileSync(archiveFile).toString()) as DataType;
return {} as DataType;
};
export { addToArchive, downloaded, makeCommand };

View file

@ -1,41 +1,41 @@
import * as shlp from 'sei-helper';
import path from 'path';
import { AvailableFilenameVars } from './module.args';
export type Variable = ({
type: 'number',
replaceWith: number
} | {
type: 'string',
replaceWith: string
}) & {
name: AvailableFilenameVars
}
const parseFileName = (input: string, variables: Variable[], numbers: number): string[] => {
const varRegex = /\${[A-Za-z1-9]+}/g;
const vars = input.match(varRegex);
if (!vars)
return [input];
for (let i = 0; i < vars.length; i++) {
const type = vars[i];
const varName = type.slice(2, -1);
const use = variables.find(a => a.name === varName);
if (use === undefined) {
console.log(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`);
continue;
}
if (use.type === 'number') {
const len = use.replaceWith.toFixed(0).length;
const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith.toFixed(0);
input = input.replace(type, replaceStr);
} else {
input = input.replace(type, use.replaceWith);
}
}
return input.split(path.sep).map(a => shlp.cleanupFilename(a));
};
import * as shlp from 'sei-helper';
import path from 'path';
import { AvailableFilenameVars } from './module.args';
export type Variable = ({
type: 'number',
replaceWith: number
} | {
type: 'string',
replaceWith: string
}) & {
name: AvailableFilenameVars
}
const parseFileName = (input: string, variables: Variable[], numbers: number): string[] => {
const varRegex = /\${[A-Za-z1-9]+}/g;
const vars = input.match(varRegex);
if (!vars)
return [input];
for (let i = 0; i < vars.length; i++) {
const type = vars[i];
const varName = type.slice(2, -1);
const use = variables.find(a => a.name === varName);
if (use === undefined) {
console.log(`[ERROR] Found variable '${type}' in fileName but no values was internally found!`);
continue;
}
if (use.type === 'number') {
const len = use.replaceWith.toFixed(0).length;
const replaceStr = len < numbers ? '0'.repeat(numbers - len) + use.replaceWith : use.replaceWith.toFixed(0);
input = input.replace(type, replaceStr);
} else {
input = input.replace(type, use.replaceWith);
}
}
return input.split(path.sep).map(a => shlp.cleanupFilename(a));
};
export default parseFileName;

View file

@ -1,55 +1,55 @@
// fonts src
const root = 'https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/';
// file list
const fontFamilies = {
'Adobe Arabic': [ 'AdobeArabic-Bold.otf', ],
'Andale Mono': [ 'andalemo.ttf', ],
'Arial': [ 'arial.ttf', 'arialbd.ttf', 'arialbi.ttf', 'ariali.ttf', ],
'Arial Unicode MS': [ 'arialuni.ttf', ],
'Arial Black': [ 'ariblk.ttf', ],
'Comic Sans MS': [ 'comic.ttf', 'comicbd.ttf', ],
'Courier New': [ 'cour.ttf', 'courbd.ttf', 'courbi.ttf', 'couri.ttf', ],
'DejaVu LGC Sans Mono': [ 'DejaVuLGCSansMono-Bold.ttf', 'DejaVuLGCSansMono-BoldOblique.ttf', 'DejaVuLGCSansMono-Oblique.ttf', 'DejaVuLGCSansMono.ttf', ],
'DejaVu Sans': [ 'DejaVuSans-Bold.ttf', 'DejaVuSans-BoldOblique.ttf', 'DejaVuSans-ExtraLight.ttf', 'DejaVuSans-Oblique.ttf', 'DejaVuSans.ttf', ],
'DejaVu Sans Condensed': [ 'DejaVuSansCondensed-Bold.ttf', 'DejaVuSansCondensed-BoldOblique.ttf', 'DejaVuSansCondensed-Oblique.ttf', 'DejaVuSansCondensed.ttf', ],
'DejaVu Sans Mono': [ 'DejaVuSansMono-Bold.ttf', 'DejaVuSansMono-BoldOblique.ttf', 'DejaVuSansMono-Oblique.ttf', 'DejaVuSansMono.ttf', ],
'Georgia': [ 'georgia.ttf', 'georgiab.ttf', 'georgiai.ttf', 'georgiaz.ttf', ],
'Impact': [ 'impact.ttf', ],
'Rubik Black': [ 'Rubik-Black.ttf', 'Rubik-BlackItalic.ttf', ],
'Rubik': [ 'Rubik-Bold.ttf', 'Rubik-BoldItalic.ttf', 'Rubik-Italic.ttf', 'Rubik-Light.ttf', 'Rubik-LightItalic.ttf', 'Rubik-Medium.ttf', 'Rubik-MediumItalic.ttf', 'Rubik-Regular.ttf', ],
'Tahoma': [ 'tahoma.ttf', ],
'Times New Roman': [ 'times.ttf', 'timesbd.ttf', 'timesbi.ttf', 'timesi.ttf', ],
'Trebuchet MS': [ 'trebuc.ttf', 'trebucbd.ttf', 'trebucbi.ttf', 'trebucit.ttf', ],
'Verdana': [ 'verdana.ttf', 'verdanab.ttf', 'verdanai.ttf', 'verdanaz.ttf', ],
'Webdings': [ 'webdings.ttf', ],
};
// collect styles from ass string
function assFonts(ass: string){
const strings = ass.replace(/\r/g,'').split('\n');
const styles = [];
for(const s of strings){
if(s.match(/^Style: /)){
const addStyle = s.split(',');
styles.push(addStyle[1]);
}
}
return [...new Set(styles)];
}
// font mime type
function fontMime(fontFile: string){
if(fontFile.match(/\.otf$/)){
return 'application/vnd.ms-opentype';
}
if(fontFile.match(/\.ttf$/)){
return 'application/x-truetype-font';
}
return 'application/octet-stream';
}
export type AvailableFonts = keyof typeof fontFamilies;
// output
export { root, fontFamilies, assFonts, fontMime };
// fonts src
const root = 'https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/';
// file list
const fontFamilies = {
'Adobe Arabic': [ 'AdobeArabic-Bold.otf', ],
'Andale Mono': [ 'andalemo.ttf', ],
'Arial': [ 'arial.ttf', 'arialbd.ttf', 'arialbi.ttf', 'ariali.ttf', ],
'Arial Unicode MS': [ 'arialuni.ttf', ],
'Arial Black': [ 'ariblk.ttf', ],
'Comic Sans MS': [ 'comic.ttf', 'comicbd.ttf', ],
'Courier New': [ 'cour.ttf', 'courbd.ttf', 'courbi.ttf', 'couri.ttf', ],
'DejaVu LGC Sans Mono': [ 'DejaVuLGCSansMono-Bold.ttf', 'DejaVuLGCSansMono-BoldOblique.ttf', 'DejaVuLGCSansMono-Oblique.ttf', 'DejaVuLGCSansMono.ttf', ],
'DejaVu Sans': [ 'DejaVuSans-Bold.ttf', 'DejaVuSans-BoldOblique.ttf', 'DejaVuSans-ExtraLight.ttf', 'DejaVuSans-Oblique.ttf', 'DejaVuSans.ttf', ],
'DejaVu Sans Condensed': [ 'DejaVuSansCondensed-Bold.ttf', 'DejaVuSansCondensed-BoldOblique.ttf', 'DejaVuSansCondensed-Oblique.ttf', 'DejaVuSansCondensed.ttf', ],
'DejaVu Sans Mono': [ 'DejaVuSansMono-Bold.ttf', 'DejaVuSansMono-BoldOblique.ttf', 'DejaVuSansMono-Oblique.ttf', 'DejaVuSansMono.ttf', ],
'Georgia': [ 'georgia.ttf', 'georgiab.ttf', 'georgiai.ttf', 'georgiaz.ttf', ],
'Impact': [ 'impact.ttf', ],
'Rubik Black': [ 'Rubik-Black.ttf', 'Rubik-BlackItalic.ttf', ],
'Rubik': [ 'Rubik-Bold.ttf', 'Rubik-BoldItalic.ttf', 'Rubik-Italic.ttf', 'Rubik-Light.ttf', 'Rubik-LightItalic.ttf', 'Rubik-Medium.ttf', 'Rubik-MediumItalic.ttf', 'Rubik-Regular.ttf', ],
'Tahoma': [ 'tahoma.ttf', ],
'Times New Roman': [ 'times.ttf', 'timesbd.ttf', 'timesbi.ttf', 'timesi.ttf', ],
'Trebuchet MS': [ 'trebuc.ttf', 'trebucbd.ttf', 'trebucbi.ttf', 'trebucit.ttf', ],
'Verdana': [ 'verdana.ttf', 'verdanab.ttf', 'verdanai.ttf', 'verdanaz.ttf', ],
'Webdings': [ 'webdings.ttf', ],
};
// collect styles from ass string
function assFonts(ass: string){
const strings = ass.replace(/\r/g,'').split('\n');
const styles = [];
for(const s of strings){
if(s.match(/^Style: /)){
const addStyle = s.split(',');
styles.push(addStyle[1]);
}
}
return [...new Set(styles)];
}
// font mime type
function fontMime(fontFile: string){
if(fontFile.match(/\.otf$/)){
return 'application/vnd.ms-opentype';
}
if(fontFile.match(/\.ttf$/)){
return 'application/x-truetype-font';
}
return 'application/octet-stream';
}
export type AvailableFonts = keyof typeof fontFamilies;
// output
export { root, fontFamilies, assFonts, fontMime };

View file

@ -1,130 +1,130 @@
import got, { OptionsOfUnknownResponseBody, ReadError, Response, ResponseType } from 'got';
// Used for future updates
// const argv = require('../funi').argv;
//
// const lang = {
// 'ptBR': {
// langCode: 'pt-BR',
// regionCode: 'BR'
// },
// 'esLA': {
// langCode: 'es-LA',
// regionCode: 'MX'
// }
// };
export type Options = {
url: string,
responseType?: ResponseType,
baseUrl?: string,
querystring?: Record<string, any>,
auth?: {
user: string,
pass: string
},
useToken?: boolean,
token?: string|boolean,
dinstid?: boolean|string,
debug?: boolean
}
// TODO convert to class
const getData = async <T = string>(options: Options) => {
const regionHeaders = {};
const gOptions = {
url: options.url,
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0',
'Accept-Encoding': 'gzip',
...regionHeaders
}
} as OptionsOfUnknownResponseBody;
if(options.responseType) {
gOptions.responseType = options.responseType;
}
if(options.baseUrl){
gOptions.prefixUrl = options.baseUrl;
gOptions.url = gOptions.url?.toString().replace(/^\//,'');
}
if(options.querystring){
gOptions.url += `?${new URLSearchParams(options.querystring).toString()}`;
}
if(options.auth){
gOptions.method = 'POST';
const newHeaders = {
...gOptions.headers,
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Origin': 'https://ww.funimation.com',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding': 'gzip, deflate, br',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0'
};
gOptions.headers = newHeaders;
gOptions.body = `username=${encodeURIComponent(options.auth.user)}&password=${encodeURIComponent(options.auth.pass)}`;
}
if(options.useToken && options.token){
gOptions.headers = {
...gOptions.headers,
Authorization: `Token ${options.token}`
};
}
if(options.dinstid){
gOptions.headers = {
...gOptions.headers,
devicetype: 'Android Phone'
};
}
// debug
gOptions.hooks = {
beforeRequest: [
(gotOpts) => {
if(options.debug){
console.log('[DEBUG] GOT OPTIONS:');
console.log(gotOpts);
}
}
]
};
try {
const res = await got(gOptions);
if(res.body && (options.responseType !== 'buffer' && (res.body as string).match(/^</))){
throw { name: 'HTMLError', res };
}
return {
ok: true,
res: {
...res,
body: res.body as T
},
};
}
catch(_error){
const error = _error as {
name: string,
} & ReadError & {
res: Response<unknown>
};
if(options.debug){
console.log(error);
}
if(error.response && error.response.statusCode && error.response.statusMessage){
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
}
else if(error.name && error.name == 'HTMLError' && error.res && error.res.body){
console.log(`[ERROR] ${error.name}:`);
console.log(error.res.body);
}
else{
console.log(`[ERROR] ${error.name}: ${error.code||error.message}`);
}
return {
ok: false,
error,
};
}
};
export default getData;
import got, { OptionsOfUnknownResponseBody, ReadError, Response, ResponseType } from 'got';
// Used for future updates
// const argv = require('../funi').argv;
//
// const lang = {
// 'ptBR': {
// langCode: 'pt-BR',
// regionCode: 'BR'
// },
// 'esLA': {
// langCode: 'es-LA',
// regionCode: 'MX'
// }
// };
export type Options = {
url: string,
responseType?: ResponseType,
baseUrl?: string,
querystring?: Record<string, any>,
auth?: {
user: string,
pass: string
},
useToken?: boolean,
token?: string|boolean,
dinstid?: boolean|string,
debug?: boolean
}
// TODO convert to class
const getData = async <T = string>(options: Options) => {
const regionHeaders = {};
const gOptions = {
url: options.url,
headers: {
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:70.0) Gecko/20100101 Firefox/70.0',
'Accept-Encoding': 'gzip',
...regionHeaders
}
} as OptionsOfUnknownResponseBody;
if(options.responseType) {
gOptions.responseType = options.responseType;
}
if(options.baseUrl){
gOptions.prefixUrl = options.baseUrl;
gOptions.url = gOptions.url?.toString().replace(/^\//,'');
}
if(options.querystring){
gOptions.url += `?${new URLSearchParams(options.querystring).toString()}`;
}
if(options.auth){
gOptions.method = 'POST';
const newHeaders = {
...gOptions.headers,
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'Origin': 'https://ww.funimation.com',
'Accept': 'application/json, text/javascript, */*; q=0.01',
'Accept-Encoding': 'gzip, deflate, br',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0'
};
gOptions.headers = newHeaders;
gOptions.body = `username=${encodeURIComponent(options.auth.user)}&password=${encodeURIComponent(options.auth.pass)}`;
}
if(options.useToken && options.token){
gOptions.headers = {
...gOptions.headers,
Authorization: `Token ${options.token}`
};
}
if(options.dinstid){
gOptions.headers = {
...gOptions.headers,
devicetype: 'Android Phone'
};
}
// debug
gOptions.hooks = {
beforeRequest: [
(gotOpts) => {
if(options.debug){
console.log('[DEBUG] GOT OPTIONS:');
console.log(gotOpts);
}
}
]
};
try {
const res = await got(gOptions);
if(res.body && (options.responseType !== 'buffer' && (res.body as string).match(/^</))){
throw { name: 'HTMLError', res };
}
return {
ok: true,
res: {
...res,
body: res.body as T
},
};
}
catch(_error){
const error = _error as {
name: string,
} & ReadError & {
res: Response<unknown>
};
if(options.debug){
console.log(error);
}
if(error.response && error.response.statusCode && error.response.statusMessage){
console.log(`[ERROR] ${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
}
else if(error.name && error.name == 'HTMLError' && error.res && error.res.body){
console.log(`[ERROR] ${error.name}:`);
console.log(error.res.body);
}
else{
console.log(`[ERROR] ${error.name}: ${error.code||error.message}`);
}
return {
ok: false,
error,
};
}
};
export default getData;

View file

@ -1,182 +1,182 @@
// available langs
export type LanguageItem = {
cr_locale?: string,
locale: string,
code: string,
name: string,
language?: string,
funi_locale?: string,
funi_name?: string
}
const languages: LanguageItem[] = [
{ cr_locale: 'es-LA', funi_name: 'Spanish (Latin Am)', funi_locale: 'esLA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-419', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-ES', locale: 'es', code: 'spa', name: 'Spanish' },
{ cr_locale: 'pt-BR', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
{ cr_locale: 'fr-FR', locale: 'fr', code: 'fra', name: 'French' },
{ cr_locale: 'de-DE', locale: 'de', code: 'deu', name: 'German' },
{ cr_locale: 'ar-ME', locale: 'ar', code: 'ara', name: 'Arabic' },
{ cr_locale: 'ar-SA', locale: 'ar', code: 'ara', name: 'Arabic' },
{ cr_locale: 'it-IT', locale: 'it', code: 'ita', name: 'Italian' },
{ cr_locale: 'ru-RU', locale: 'ru', code: 'rus', name: 'Russian' },
{ cr_locale: 'tr-TR', locale: 'tr', code: 'tur', name: 'Turkish' },
{ funi_locale: 'zhMN', locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' },
{ cr_locale: 'en-US', funi_locale: 'enUS', locale: 'en', code: 'eng', name: 'English' },
{ cr_locale: 'ja-JP', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' },
];
// add en language names
(() =>{
for(const languageIndex in languages){
if(!languages[languageIndex].language){
languages[languageIndex].language = languages[languageIndex].name;
}
}
})();
// construct dub language codes
const dubLanguageCodes = (() => {
const dubLanguageCodesArray = [];
for(const language of languages){
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
})();
// construct subtitle languages filter
const subtitleLanguagesFilter = (() => {
const subtitleLanguagesExtraParameters = ['all', 'none'];
return [...subtitleLanguagesExtraParameters, ...new Set(languages.map(l => { return l.locale; }))];
})();
const searchLocales = (() => {
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1))];
})();
// convert
const fixLanguageTag = (tag: string) => {
tag = typeof tag == 'string' ? tag : 'und';
const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/);
if(tagLangLC){
const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`;
if(findLang(tagLang).cr_locale != 'und'){
return findLang(tagLang).cr_locale;
}
else{
return tagLang;
}
}
else{
return tag;
}
};
// find lang by cr_locale
const findLang = (cr_locale: string) => {
const lang = languages.find(l => { return l.cr_locale == cr_locale; });
return lang ? lang : { cr_locale: 'und', locale: 'un', code: 'und', name: '', language: '' };
};
const fixAndFindCrLC = (cr_locale: string) => {
const str = fixLanguageTag(cr_locale);
return findLang(str || '');
};
// rss subs lang parser
const parseRssSubtitlesString = (subs: string) => {
const splitMap = subs.replace(/\s/g, '').split(',').map((s) => {
return fixAndFindCrLC(s).locale;
});
const sort = sortTags(splitMap);
return sort.join(', ');
};
// parse subtitles Array
const parseSubtitlesArray = (tags: string[]) => {
const sort = sortSubtitles(tags.map((t) => {
return { locale: fixAndFindCrLC(t).locale };
}));
return sort.map((t) => { return t.locale; }).join(', ');
};
// sort subtitles
const sortSubtitles = <T extends {
[key: string]: unknown
} = Record<string, string>> (data: T[], sortkey?: keyof T) : T[] => {
const idx: Record<string, number> = {};
const key = sortkey || 'locale' as keyof T;
const tags = [...new Set(Object.values(languages).map(e => e.locale))];
for(const l of tags){
idx[l] = Object.keys(idx).length + 1;
}
data.sort((a, b) => {
const ia = idx[a[key] as string] ? idx[a[key] as string] : 50;
const ib = idx[b[key] as string] ? idx[b[key] as string] : 50;
return ia - ib;
});
return data;
};
const sortTags = (data: string[]) => {
const retData = data.map(e => { return { locale: e }; });
const sort = sortSubtitles(retData);
return sort.map(e => e.locale as string);
};
const subsFile = (fnOutput:string, subsIndex: string, langItem: LanguageItem) => {
subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0');
return `${fnOutput}.${subsIndex} ${langItem.code} ${langItem.language}.ass`;
};
// construct dub langs const
const dubLanguages = (() => {
const dubDb: Record<string, string> = {};
for(const lang of languages){
if(!Object.keys(dubDb).includes(lang.name)){
dubDb[lang.name] = lang.code;
}
}
return dubDb;
})();
// dub regex
const dubRegExpStr =
`\\((${Object.keys(dubLanguages).join('|')})(?: (Dub|VO))?\\)$`;
const dubRegExp = new RegExp(dubRegExpStr);
// code to lang name
const langCode2name = (code: string) => {
const codeIdx = dubLanguageCodes.indexOf(code);
return Object.keys(dubLanguages)[codeIdx];
};
// locale to lang name
const locale2language = (locale: string) => {
const filteredLocale = languages.filter(l => {
return l.locale == locale;
});
return filteredLocale[0];
};
// output
export {
languages,
dubLanguageCodes,
dubLanguages,
langCode2name,
locale2language,
dubRegExp,
subtitleLanguagesFilter,
searchLocales,
fixLanguageTag,
findLang,
fixAndFindCrLC,
parseRssSubtitlesString,
parseSubtitlesArray,
sortSubtitles,
sortTags,
subsFile,
};
// available langs
export type LanguageItem = {
cr_locale?: string,
locale: string,
code: string,
name: string,
language?: string,
funi_locale?: string,
funi_name?: string
}
const languages: LanguageItem[] = [
{ cr_locale: 'es-LA', funi_name: 'Spanish (Latin Am)', funi_locale: 'esLA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-419', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
{ cr_locale: 'es-ES', locale: 'es', code: 'spa', name: 'Spanish' },
{ cr_locale: 'pt-BR', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
{ cr_locale: 'fr-FR', locale: 'fr', code: 'fra', name: 'French' },
{ cr_locale: 'de-DE', locale: 'de', code: 'deu', name: 'German' },
{ cr_locale: 'ar-ME', locale: 'ar', code: 'ara', name: 'Arabic' },
{ cr_locale: 'ar-SA', locale: 'ar', code: 'ara', name: 'Arabic' },
{ cr_locale: 'it-IT', locale: 'it', code: 'ita', name: 'Italian' },
{ cr_locale: 'ru-RU', locale: 'ru', code: 'rus', name: 'Russian' },
{ cr_locale: 'tr-TR', locale: 'tr', code: 'tur', name: 'Turkish' },
{ funi_locale: 'zhMN', locale: 'zh', code: 'cmn', name: 'Chinese (Mandarin, PRC)' },
{ cr_locale: 'en-US', funi_locale: 'enUS', locale: 'en', code: 'eng', name: 'English' },
{ cr_locale: 'ja-JP', funi_locale: 'jaJP', locale: 'ja', code: 'jpn', name: 'Japanese' },
];
// add en language names
(() =>{
for(const languageIndex in languages){
if(!languages[languageIndex].language){
languages[languageIndex].language = languages[languageIndex].name;
}
}
})();
// construct dub language codes
const dubLanguageCodes = (() => {
const dubLanguageCodesArray = [];
for(const language of languages){
dubLanguageCodesArray.push(language.code);
}
return [...new Set(dubLanguageCodesArray)];
})();
// construct subtitle languages filter
const subtitleLanguagesFilter = (() => {
const subtitleLanguagesExtraParameters = ['all', 'none'];
return [...subtitleLanguagesExtraParameters, ...new Set(languages.map(l => { return l.locale; }))];
})();
const searchLocales = (() => {
return ['', ...new Set(languages.map(l => { return l.cr_locale; }).slice(0, -1))];
})();
// convert
const fixLanguageTag = (tag: string) => {
tag = typeof tag == 'string' ? tag : 'und';
const tagLangLC = tag.match(/^(\w{2})-?(\w{2})$/);
if(tagLangLC){
const tagLang = `${tagLangLC[1]}-${tagLangLC[2].toUpperCase()}`;
if(findLang(tagLang).cr_locale != 'und'){
return findLang(tagLang).cr_locale;
}
else{
return tagLang;
}
}
else{
return tag;
}
};
// find lang by cr_locale
const findLang = (cr_locale: string) => {
const lang = languages.find(l => { return l.cr_locale == cr_locale; });
return lang ? lang : { cr_locale: 'und', locale: 'un', code: 'und', name: '', language: '' };
};
const fixAndFindCrLC = (cr_locale: string) => {
const str = fixLanguageTag(cr_locale);
return findLang(str || '');
};
// rss subs lang parser
const parseRssSubtitlesString = (subs: string) => {
const splitMap = subs.replace(/\s/g, '').split(',').map((s) => {
return fixAndFindCrLC(s).locale;
});
const sort = sortTags(splitMap);
return sort.join(', ');
};
// parse subtitles Array
const parseSubtitlesArray = (tags: string[]) => {
const sort = sortSubtitles(tags.map((t) => {
return { locale: fixAndFindCrLC(t).locale };
}));
return sort.map((t) => { return t.locale; }).join(', ');
};
// sort subtitles
const sortSubtitles = <T extends {
[key: string]: unknown
} = Record<string, string>> (data: T[], sortkey?: keyof T) : T[] => {
const idx: Record<string, number> = {};
const key = sortkey || 'locale' as keyof T;
const tags = [...new Set(Object.values(languages).map(e => e.locale))];
for(const l of tags){
idx[l] = Object.keys(idx).length + 1;
}
data.sort((a, b) => {
const ia = idx[a[key] as string] ? idx[a[key] as string] : 50;
const ib = idx[b[key] as string] ? idx[b[key] as string] : 50;
return ia - ib;
});
return data;
};
const sortTags = (data: string[]) => {
const retData = data.map(e => { return { locale: e }; });
const sort = sortSubtitles(retData);
return sort.map(e => e.locale as string);
};
const subsFile = (fnOutput:string, subsIndex: string, langItem: LanguageItem) => {
subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0');
return `${fnOutput}.${subsIndex} ${langItem.code} ${langItem.language}.ass`;
};
// construct dub langs const
const dubLanguages = (() => {
const dubDb: Record<string, string> = {};
for(const lang of languages){
if(!Object.keys(dubDb).includes(lang.name)){
dubDb[lang.name] = lang.code;
}
}
return dubDb;
})();
// dub regex
const dubRegExpStr =
`\\((${Object.keys(dubLanguages).join('|')})(?: (Dub|VO))?\\)$`;
const dubRegExp = new RegExp(dubRegExpStr);
// code to lang name
const langCode2name = (code: string) => {
const codeIdx = dubLanguageCodes.indexOf(code);
return Object.keys(dubLanguages)[codeIdx];
};
// locale to lang name
const locale2language = (locale: string) => {
const filteredLocale = languages.filter(l => {
return l.locale == locale;
});
return filteredLocale[0];
};
// output
export {
languages,
dubLanguageCodes,
dubLanguages,
langCode2name,
locale2language,
dubRegExp,
subtitleLanguagesFilter,
searchLocales,
fixLanguageTag,
findLang,
fixAndFindCrLC,
parseRssSubtitlesString,
parseSubtitlesArray,
sortSubtitles,
sortTags,
subsFile,
};

View file

@ -1,287 +1,287 @@
import * as iso639 from 'iso-639';
import { fontFamilies, fontMime } from './module.fontsData';
import path from 'path';
import fs from 'fs';
import { LanguageItem } from './module.langsData';
import { AvailableMuxer } from './module.args';
export type MergerInput = {
path: string,
lang: LanguageItem
}
export type SubtitleInput = {
language: LanguageItem,
file: string,
closedCaption?: boolean
}
export type Font = keyof typeof fontFamilies;
export type ParsedFont = {
name: string,
path: string,
mime: string,
}
export type MergerOptions = {
videoAndAudio: MergerInput[],
onlyVid: MergerInput[],
onlyAudio: MergerInput[],
subtitles: SubtitleInput[],
output: string,
simul?: boolean,
fonts?: ParsedFont[],
skipSubMux?: boolean,
coustomOptions?: string,
}
class Merger {
constructor(private options: MergerOptions) {
if (this.options.skipSubMux)
this.options.subtitles = [];
}
public FFmpeg() : string {
const args = [];
const metaData = [];
let index = 0;
let audioIndex = 0;
let hasVideo = false;
for (const vid of this.options.videoAndAudio) {
args.push(`-i "${vid.path}"`);
if (!hasVideo) {
metaData.push(`-map ${index}:a -map ${index}:v`);
metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`);
metaData.push(`-metadata:s:v:${index} title="[Video Stream]"`);
hasVideo = true;
} else {
metaData.push(`-map ${index}:a`);
metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`);
}
audioIndex++;
index++;
}
for (const vid of this.options.onlyVid) {
if (!hasVideo) {
args.push(`-i "${vid.path}"`);
metaData.push(`-map ${index} -map -${index}:a`);
metaData.push(`-metadata:s:v:${index} title="[Video Stream]"`);
hasVideo = true;
index++;
}
}
for (const aud of this.options.onlyAudio) {
args.push(`-i "${aud.path}"`);
metaData.push(`-map ${index}`);
metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`);
index++;
audioIndex++;
}
for (const index in this.options.subtitles) {
const sub = this.options.subtitles[index];
args.push(`-i "${sub.file}"`);
}
if (this.options.output.split('.').pop() === 'mkv')
if (this.options.fonts) {
let fontIndex = 0;
for (const font of this.options.fonts) {
args.push(`-attach ${font.path} -metadata:s:t:${fontIndex} mimetype=${font.mime}`);
fontIndex++;
}
}
args.push(...metaData);
args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
args.push(
'-c:v copy',
'-c:a copy',
this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass',
...this.options.subtitles.map((sub, subindex) => `-metadata:s:s:${subindex} title="${
(sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ' CC' : ''}`
}" -metadata:s:s:${subindex} language=${sub.language.code}`),
this.options.coustomOptions ?? ''
);
args.push(`"${this.options.output}"`);
return args.join(' ');
}
public static getLanguageCode = (from: string, _default = 'eng'): string => {
if (from === 'cmn') return 'chi';
for (const lang in iso639.iso_639_2) {
const langObj = iso639.iso_639_2[lang];
if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) {
return langObj['639-2'] as string;
}
}
return _default;
};
public MkvMerge = () => {
const args = [];
let hasVideo = false;
args.push(`-o "${this.options.output}"`);
args.push(
'--no-date',
'--disable-track-statistics-tags',
'--engage no_variable_data',
);
for (const vid of this.options.onlyVid) {
if (!hasVideo) {
args.push(
'--video-tracks 0',
'--no-audio'
);
const trackName = (vid.lang.name + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
args.push('--track-name', `0:"${trackName}"`);
args.push(`--language 0:${vid.lang.code}`);
hasVideo = true;
args.push(`"${vid.path}"`);
}
}
for (const vid of this.options.videoAndAudio) {
if (!hasVideo) {
args.push(
'--video-tracks 0',
'--audio-tracks 1'
);
const trackName = (vid.lang.name + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
args.push('--track-name', `0:"${trackName}"`);
args.push('--track-name', `1:"${trackName}"`);
args.push(`--language 1:${vid.lang.code}`);
hasVideo = true;
} else {
args.push(
'--no-video',
'--audio-tracks 1'
);
const trackName = (vid.lang.name + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
args.push('--track-name', `1:"${trackName}"`);
args.push(`--language 1:${vid.lang.code}`);
}
args.push(`"${vid.path}"`);
}
for (const aud of this.options.onlyAudio) {
const trackName = aud.lang.name;
args.push('--track-name', `0:"${trackName}"`);
args.push(`--language 0:${aud.lang.code}`);
args.push(
'--no-video',
'--audio-tracks 0'
);
args.push(`"${aud.path}"`);
}
if (this.options.subtitles.length > 0) {
for (const subObj of this.options.subtitles) {
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ' CC' : ''}`}"`);
args.push('--language', `0:"${subObj.language.code}"`);
args.push(`"${subObj.file}"`);
}
} else {
args.push(
'--no-subtitles',
);
}
if (this.options.fonts && this.options.fonts.length > 0) {
for (const f of this.options.fonts) {
console.log(f.path);
args.push('--attachment-name', f.name);
args.push('--attachment-mime-type', f.mime);
args.push('--attach-file', `"${f.path}"`);
}
} else {
args.push(
'--no-attachments'
);
}
return args.join(' ');
};
public static checkMerger(bin: {
mkvmerge?: string,
ffmpeg?: string,
}, useMP4format: boolean, force: AvailableMuxer|undefined) : {
MKVmerge?: string,
FFmpeg?: string
} {
if (force && bin[force]) {
return {
FFmpeg: force === 'ffmpeg' ? bin.ffmpeg : undefined,
MKVmerge: force === 'mkvmerge' ? bin.mkvmerge : undefined
};
}
if (useMP4format && bin.ffmpeg) {
return {
FFmpeg: bin.ffmpeg
};
} else if (!useMP4format && (bin.mkvmerge || bin.ffmpeg)) {
return {
MKVmerge: bin.mkvmerge,
FFmpeg: bin.ffmpeg
};
} else if (useMP4format) {
console.log('[WARN] FFmpeg not found, skip muxing...');
} else if (!bin.mkvmerge) {
console.log('[WARN] MKVMerge not found, skip muxing...');
}
return {};
}
public static makeFontsList (fontsDir: string, subs: {
language: LanguageItem,
fonts: Font[]
}[]) : ParsedFont[] {
let fontsNameList: Font[] = []; const fontsList = [], subsList = []; let isNstr = true;
for(const s of subs){
fontsNameList.push(...s.fonts);
subsList.push(s.language.locale);
}
fontsNameList = [...new Set(fontsNameList)];
if(subsList.length > 0){
console.log('\n[INFO] Subtitles: %s (Total: %s)', subsList.join(', '), subsList.length);
isNstr = false;
}
if(fontsNameList.length > 0){
console.log((isNstr ? '\n' : '') + '[INFO] Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length);
}
for(const f of fontsNameList){
const fontFiles = fontFamilies[f];
if(fontFiles){
for (const fontFile of fontFiles) {
const fontPath = path.join(fontsDir, fontFile);
const mime = fontMime(fontFile);
if(fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0){
fontsList.push({
name: fontFile,
path: fontPath,
mime: mime,
});
}
}
}
}
return fontsList;
}
public cleanUp() {
this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path));
this.options.subtitles.forEach(a => fs.unlinkSync(a.file));
}
}
import * as iso639 from 'iso-639';
import { fontFamilies, fontMime } from './module.fontsData';
import path from 'path';
import fs from 'fs';
import { LanguageItem } from './module.langsData';
import { AvailableMuxer } from './module.args';
export type MergerInput = {
path: string,
lang: LanguageItem
}
export type SubtitleInput = {
language: LanguageItem,
file: string,
closedCaption?: boolean
}
export type Font = keyof typeof fontFamilies;
export type ParsedFont = {
name: string,
path: string,
mime: string,
}
export type MergerOptions = {
videoAndAudio: MergerInput[],
onlyVid: MergerInput[],
onlyAudio: MergerInput[],
subtitles: SubtitleInput[],
output: string,
simul?: boolean,
fonts?: ParsedFont[],
skipSubMux?: boolean,
coustomOptions?: string,
}
class Merger {
constructor(private options: MergerOptions) {
if (this.options.skipSubMux)
this.options.subtitles = [];
}
public FFmpeg() : string {
const args = [];
const metaData = [];
let index = 0;
let audioIndex = 0;
let hasVideo = false;
for (const vid of this.options.videoAndAudio) {
args.push(`-i "${vid.path}"`);
if (!hasVideo) {
metaData.push(`-map ${index}:a -map ${index}:v`);
metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`);
metaData.push(`-metadata:s:v:${index} title="[Video Stream]"`);
hasVideo = true;
} else {
metaData.push(`-map ${index}:a`);
metaData.push(`-metadata:s:a:${audioIndex} language=${vid.lang.code}`);
}
audioIndex++;
index++;
}
for (const vid of this.options.onlyVid) {
if (!hasVideo) {
args.push(`-i "${vid.path}"`);
metaData.push(`-map ${index} -map -${index}:a`);
metaData.push(`-metadata:s:v:${index} title="[Video Stream]"`);
hasVideo = true;
index++;
}
}
for (const aud of this.options.onlyAudio) {
args.push(`-i "${aud.path}"`);
metaData.push(`-map ${index}`);
metaData.push(`-metadata:s:a:${audioIndex} language=${aud.lang.code}`);
index++;
audioIndex++;
}
for (const index in this.options.subtitles) {
const sub = this.options.subtitles[index];
args.push(`-i "${sub.file}"`);
}
if (this.options.output.split('.').pop() === 'mkv')
if (this.options.fonts) {
let fontIndex = 0;
for (const font of this.options.fonts) {
args.push(`-attach ${font.path} -metadata:s:t:${fontIndex} mimetype=${font.mime}`);
fontIndex++;
}
}
args.push(...metaData);
args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
args.push(
'-c:v copy',
'-c:a copy',
this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass',
...this.options.subtitles.map((sub, subindex) => `-metadata:s:s:${subindex} title="${
(sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ' CC' : ''}`
}" -metadata:s:s:${subindex} language=${sub.language.code}`),
this.options.coustomOptions ?? ''
);
args.push(`"${this.options.output}"`);
return args.join(' ');
}
public static getLanguageCode = (from: string, _default = 'eng'): string => {
if (from === 'cmn') return 'chi';
for (const lang in iso639.iso_639_2) {
const langObj = iso639.iso_639_2[lang];
if (Object.prototype.hasOwnProperty.call(langObj, '639-1') && langObj['639-1'] === from) {
return langObj['639-2'] as string;
}
}
return _default;
};
public MkvMerge = () => {
const args = [];
let hasVideo = false;
args.push(`-o "${this.options.output}"`);
args.push(
'--no-date',
'--disable-track-statistics-tags',
'--engage no_variable_data',
);
for (const vid of this.options.onlyVid) {
if (!hasVideo) {
args.push(
'--video-tracks 0',
'--no-audio'
);
const trackName = (vid.lang.name + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
args.push('--track-name', `0:"${trackName}"`);
args.push(`--language 0:${vid.lang.code}`);
hasVideo = true;
args.push(`"${vid.path}"`);
}
}
for (const vid of this.options.videoAndAudio) {
if (!hasVideo) {
args.push(
'--video-tracks 0',
'--audio-tracks 1'
);
const trackName = (vid.lang.name + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
args.push('--track-name', `0:"${trackName}"`);
args.push('--track-name', `1:"${trackName}"`);
args.push(`--language 1:${vid.lang.code}`);
hasVideo = true;
} else {
args.push(
'--no-video',
'--audio-tracks 1'
);
const trackName = (vid.lang.name + (this.options.simul ? ' [Simulcast]' : ' [Uncut]'));
args.push('--track-name', `1:"${trackName}"`);
args.push(`--language 1:${vid.lang.code}`);
}
args.push(`"${vid.path}"`);
}
for (const aud of this.options.onlyAudio) {
const trackName = aud.lang.name;
args.push('--track-name', `0:"${trackName}"`);
args.push(`--language 0:${aud.lang.code}`);
args.push(
'--no-video',
'--audio-tracks 0'
);
args.push(`"${aud.path}"`);
}
if (this.options.subtitles.length > 0) {
for (const subObj of this.options.subtitles) {
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ' CC' : ''}`}"`);
args.push('--language', `0:"${subObj.language.code}"`);
args.push(`"${subObj.file}"`);
}
} else {
args.push(
'--no-subtitles',
);
}
if (this.options.fonts && this.options.fonts.length > 0) {
for (const f of this.options.fonts) {
console.log(f.path);
args.push('--attachment-name', f.name);
args.push('--attachment-mime-type', f.mime);
args.push('--attach-file', `"${f.path}"`);
}
} else {
args.push(
'--no-attachments'
);
}
return args.join(' ');
};
public static checkMerger(bin: {
mkvmerge?: string,
ffmpeg?: string,
}, useMP4format: boolean, force: AvailableMuxer|undefined) : {
MKVmerge?: string,
FFmpeg?: string
} {
if (force && bin[force]) {
return {
FFmpeg: force === 'ffmpeg' ? bin.ffmpeg : undefined,
MKVmerge: force === 'mkvmerge' ? bin.mkvmerge : undefined
};
}
if (useMP4format && bin.ffmpeg) {
return {
FFmpeg: bin.ffmpeg
};
} else if (!useMP4format && (bin.mkvmerge || bin.ffmpeg)) {
return {
MKVmerge: bin.mkvmerge,
FFmpeg: bin.ffmpeg
};
} else if (useMP4format) {
console.log('[WARN] FFmpeg not found, skip muxing...');
} else if (!bin.mkvmerge) {
console.log('[WARN] MKVMerge not found, skip muxing...');
}
return {};
}
public static makeFontsList (fontsDir: string, subs: {
language: LanguageItem,
fonts: Font[]
}[]) : ParsedFont[] {
let fontsNameList: Font[] = []; const fontsList = [], subsList = []; let isNstr = true;
for(const s of subs){
fontsNameList.push(...s.fonts);
subsList.push(s.language.locale);
}
fontsNameList = [...new Set(fontsNameList)];
if(subsList.length > 0){
console.log('\n[INFO] Subtitles: %s (Total: %s)', subsList.join(', '), subsList.length);
isNstr = false;
}
if(fontsNameList.length > 0){
console.log((isNstr ? '\n' : '') + '[INFO] Required fonts: %s (Total: %s)', fontsNameList.join(', '), fontsNameList.length);
}
for(const f of fontsNameList){
const fontFiles = fontFamilies[f];
if(fontFiles){
for (const fontFile of fontFiles) {
const fontPath = path.join(fontsDir, fontFile);
const mime = fontMime(fontFile);
if(fs.existsSync(fontPath) && fs.statSync(fontPath).size != 0){
fontsList.push({
name: fontFile,
path: fontPath,
mime: mime,
});
}
}
}
}
return fontsList;
}
public cleanUp() {
this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path));
this.options.subtitles.forEach(a => fs.unlinkSync(a.file));
}
}
export default Merger;

View file

@ -1,105 +1,105 @@
const parseSelect = (selectString: string, but = false) : {
isSelected: (val: string|string[]) => boolean,
values: string[]
} => {
if (!selectString)
return {
values: [],
isSelected: () => but
};
const parts = selectString.split(',');
const select: string[] = [];
parts.forEach(part => {
if (part.includes('-')) {
const splits = part.split('-');
if (splits.length !== 2) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
const firstPart = splits[0];
const match = firstPart.match(/[A-Za-z]+/);
if (match && match.length > 0) {
if (match.index && match.index !== 0) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
const letters = firstPart.substring(0, match[0].length);
const number = parseInt(firstPart.substring(match[0].length));
const b = parseInt(splits[1]);
if (isNaN(number) || isNaN(b)) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
for (let i = number; i <= b; i++) {
select.push(`${letters}${i}`);
}
} else {
const a = parseInt(firstPart);
const b = parseInt(splits[1]);
if (isNaN(a) || isNaN(b)) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
for (let i = a; i <= b; i++) {
select.push(`${i}`);
}
}
} else {
if (part.match(/[0-9A-Z]{9}/)) {
select.push(part);
return;
}
const match = part.match(/[A-Za-z]+/);
if (match && match.length > 0) {
if (match.index && match.index !== 0) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
const letters = part.substring(0, match[0].length);
const number = parseInt(part.substring(match[0].length));
if (isNaN(number)) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
select.push(`${letters}${number}`);
} else {
select.push(`${parseInt(part)}`);
}
}
});
return {
values: select,
isSelected: (st) => {
if (typeof st === 'string')
st = [st];
return st.some(st => {
const match = st.match(/[A-Za-z]+/);
if (st.match(/[0-9A-Z]{9}/)) {
const included = select.includes(st);
return but ? !included : included;
} else if (match && match.length > 0) {
if (match.index && match.index !== 0) {
return false;
}
const letter = st.substring(0, match[0].length);
const number = parseInt(st.substring(match[0].length));
if (isNaN(number)) {
return false;
}
const included = select.includes(`${letter}${number}`);
return but ? !included : included;
} else {
const included = select.includes(`${parseInt(st)}`);
return but ? !included : included;
}
});
}
};
};
const parseSelect = (selectString: string, but = false) : {
isSelected: (val: string|string[]) => boolean,
values: string[]
} => {
if (!selectString)
return {
values: [],
isSelected: () => but
};
const parts = selectString.split(',');
const select: string[] = [];
parts.forEach(part => {
if (part.includes('-')) {
const splits = part.split('-');
if (splits.length !== 2) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
const firstPart = splits[0];
const match = firstPart.match(/[A-Za-z]+/);
if (match && match.length > 0) {
if (match.index && match.index !== 0) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
const letters = firstPart.substring(0, match[0].length);
const number = parseInt(firstPart.substring(match[0].length));
const b = parseInt(splits[1]);
if (isNaN(number) || isNaN(b)) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
for (let i = number; i <= b; i++) {
select.push(`${letters}${i}`);
}
} else {
const a = parseInt(firstPart);
const b = parseInt(splits[1]);
if (isNaN(a) || isNaN(b)) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
for (let i = a; i <= b; i++) {
select.push(`${i}`);
}
}
} else {
if (part.match(/[0-9A-Z]{9}/)) {
select.push(part);
return;
}
const match = part.match(/[A-Za-z]+/);
if (match && match.length > 0) {
if (match.index && match.index !== 0) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
const letters = part.substring(0, match[0].length);
const number = parseInt(part.substring(match[0].length));
if (isNaN(number)) {
console.log(`[WARN] Unable to parse input "${part}"`);
return;
}
select.push(`${letters}${number}`);
} else {
select.push(`${parseInt(part)}`);
}
}
});
return {
values: select,
isSelected: (st) => {
if (typeof st === 'string')
st = [st];
return st.some(st => {
const match = st.match(/[A-Za-z]+/);
if (st.match(/[0-9A-Z]{9}/)) {
const included = select.includes(st);
return but ? !included : included;
} else if (match && match.length > 0) {
if (match.index && match.index !== 0) {
return false;
}
const letter = st.substring(0, match[0].length);
const number = parseInt(st.substring(match[0].length));
if (isNaN(number)) {
return false;
}
const included = select.includes(`${letter}${number}`);
return but ? !included : included;
} else {
const included = select.includes(`${parseInt(st)}`);
return but ? !included : included;
}
});
}
};
};
export default parseSelect;

View file

@ -1,234 +1,234 @@
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 debug: boolean, private nosess = false) {}
async getData<T = string> (durl: string, params?: Params) {
params = params || {};
// options
const 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;
}
// if auth
const loc = new URL(durl);
// avoid cloudflare protection
// debug
options.hooks = {
beforeRequest: [
(options) => {
if(this.debug){
console.log('[DEBUG] GOT OPTIONS:');
console.log(options);
}
}
]
};
if(this.debug){
options.curlDebug = true;
}
// try do request
try {
const 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){
const cookieUpdated = []; let lastExp = 0;
console.trace('Type of setCookie:', typeof setCookie, setCookie);
const parsedCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie);
for(const cookieName of Object.keys(parsedCookie)){
if(parsedCookie[cookieName] && parsedCookie[cookieName].value && parsedCookie[cookieName].value == 'deleted'){
delete parsedCookie[cookieName];
}
}
for(const 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(const uCookie of usefulCookies.auth){
if(!parsedCookie[uCookie]){
continue;
}
if(isAuth || parsedCookie[uCookie] && Date.now() > lastExp){
this.session[uCookie] = parsedCookie[uCookie];
cookieUpdated.push(uCookie);
}
}
for(const uCookie of usefulCookies.sess){
if(!parsedCookie[uCookie]){
continue;
}
if(
isAuth
|| this.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.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) {
const 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;
}
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;
}
export {
buildProxy,
usefulCookies,
Req,
};
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 debug: boolean, private nosess = false) {}
async getData<T = string> (durl: string, params?: Params) {
params = params || {};
// options
const 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;
}
// if auth
const loc = new URL(durl);
// avoid cloudflare protection
// debug
options.hooks = {
beforeRequest: [
(options) => {
if(this.debug){
console.log('[DEBUG] GOT OPTIONS:');
console.log(options);
}
}
]
};
if(this.debug){
options.curlDebug = true;
}
// try do request
try {
const 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){
const cookieUpdated = []; let lastExp = 0;
console.trace('Type of setCookie:', typeof setCookie, setCookie);
const parsedCookie = fileData ? cookieFile(fileData) : shlp.cookie.parse(setCookie);
for(const cookieName of Object.keys(parsedCookie)){
if(parsedCookie[cookieName] && parsedCookie[cookieName].value && parsedCookie[cookieName].value == 'deleted'){
delete parsedCookie[cookieName];
}
}
for(const 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(const uCookie of usefulCookies.auth){
if(!parsedCookie[uCookie]){
continue;
}
if(isAuth || parsedCookie[uCookie] && Date.now() > lastExp){
this.session[uCookie] = parsedCookie[uCookie];
cookieUpdated.push(uCookie);
}
}
for(const uCookie of usefulCookies.sess){
if(!parsedCookie[uCookie]){
continue;
}
if(
isAuth
|| this.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.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) {
const 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;
}
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;
}
export {
buildProxy,
usefulCookies,
Req,
};

View file

@ -1,175 +1,175 @@
import got from 'got';
import fs from 'fs';
import { GithubTag, TagCompare } from '../@types/github';
import path from 'path';
import { UpdateFile } from '../@types/updateFile';
import packageJson from '../package.json';
import { CompilerOptions, transpileModule } from 'typescript';
import tsConfig from '../tsconfig.json';
import fsextra from 'fs-extra';
import seiHelper from 'sei-helper';
const workingDir = (process as NodeJS.Process & {
pkg?: unknown
}).pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
const updateFilePlace = path.join(workingDir, 'config', 'updates.json');
const updateIgnore = [
'*.d.ts',
'.git',
'lib',
'node_modules',
'@types',
path.join('bin', 'mkvtoolnix'),
path.join('config', 'token.yml'),
'.eslint',
'tsconfig.json',
'updates.json',
'tsc.ts'
];
const askBeforeUpdate = [
'*.yml'
];
enum ApplyType {
DELETE, ADD, UPDATE
}
export type ApplyItem = {
type: ApplyType,
path: string,
content: string
}
export default (async (force = false) => {
const isPackaged = (process as NodeJS.Process & {
pkg?: unknown
}).pkg ? true : false;
if (isPackaged) {
return;
}
let updateFile: UpdateFile|undefined;
if (fs.existsSync(updateFilePlace)) {
updateFile = JSON.parse(fs.readFileSync(updateFilePlace).toString()) as UpdateFile;
if (new Date() < new Date(updateFile.nextCheck) && !force) {
return;
}
}
console.log('Checking for updates...');
const tagRequest = await got('https://api.github.com/repos/anidl/multi-downloader-nx/tags');
const tags = JSON.parse(tagRequest.body) as GithubTag[];
if (tags.length > 0) {
const newer = tags.filter(a => {
return isNewer(packageJson.version, a.name);
});
console.log(`Found ${tags.length} release tags and ${newer.length} that are new.`);
if (newer.length < 1) {
console.log('[INFO] No new tags found');
return done();
}
const newest = newer.sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0)[0];
const compareRequest = await got(`https://api.github.com/repos/anidl/multi-downloader-nx/compare/${packageJson.version}...${newest.name}`);
const compareJSON = JSON.parse(compareRequest.body) as TagCompare;
console.log(`You are behind by ${compareJSON.ahead_by} releases!`);
const changedFiles = compareJSON.files.map(a => ({
...a,
filename: path.join(...a.filename.split('/'))
})).filter(a => {
return !updateIgnore.some(_filter => matchString(_filter, a.filename));
});
if (changedFiles.length < 1) {
console.log('[INFO] No file changes found... updating package.json. If you think this is an error please get the newst version yourself.');
return done(newest.name);
}
console.log(`Found file changes: \n${changedFiles.map(a => ` [${
a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'
}] ${a.filename}`).join('\n')}`);
const remove: string[] = [];
changedFiles.filter(a => a.status !== 'added').forEach(async a => {
if (!askBeforeUpdate.some(pattern => matchString(pattern, a.filename)))
return;
const answer = await seiHelper.question(`The developer decided that the file '${a.filename}' may contain information you changed yourself. Should they be overriden to be updated? [y/N]`);
if (answer.toLowerCase() === 'y')
remove.push(a.sha);
});
const changesToApply = await Promise.all(changedFiles.filter(a => !remove.includes(a.sha)).map(async (a): Promise<ApplyItem> => {
if (a.filename.endsWith('.ts')) {
const ret = {
path: a.filename.slice(0, -2) + 'js',
content: transpileModule((await got(a.raw_url)).body, {
compilerOptions: tsConfig.compilerOptions as unknown as CompilerOptions
}).outputText,
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
};
console.log('✓ transpiled %s', ret.path);
return ret;
} else {
const ret = {
path: a.filename,
content: (await got(a.raw_url)).body,
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
};
console.log('✓ transpiled %s', ret.path);
return ret;
}
}));
changesToApply.forEach(a => {
fsextra.ensureDirSync(path.dirname(a.path));
fs.writeFileSync(path.join(__dirname, '..', a.path), a.content);
console.log('✓ written %s', a.path);
});
console.log('[INFO] Done');
return done();
}
});
function done(newVersion?: string) {
const next = new Date(Date.now() + 1000 * 60 * 60 * 24);
fs.writeFileSync(updateFilePlace, JSON.stringify({
lastCheck: Date.now(),
nextCheck: next.getTime()
} as UpdateFile, null, 2));
if (newVersion) {
fs.writeFileSync('../package.json', JSON.stringify({
...packageJson,
version: newVersion
}, null, 4));
}
console.log('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.');
}
function isNewer(curr: string, compare: string) : boolean {
const currParts = curr.split('.').map(a => parseInt(a));
const compareParts = compare.split('.').map(a => parseInt(a));
for (let i = 0; i < Math.max(currParts.length, compareParts.length); i++) {
if (currParts.length <= i)
return true;
if (compareParts.length <= i)
return false;
if (currParts[i] !== compareParts[i])
return compareParts[i] > currParts[i];
}
return false;
}
function matchString(pattern: string, toMatch: string) : boolean {
const filter = path.join('..', pattern);
if (pattern.startsWith('*')) {
return toMatch.endsWith(pattern.slice(1));
} else if (filter.split(path.sep).pop()?.indexOf('.') === -1) {
return toMatch.startsWith(filter);
} else {
return toMatch.split(path.sep).pop() === pattern;
}
import got from 'got';
import fs from 'fs';
import { GithubTag, TagCompare } from '../@types/github';
import path from 'path';
import { UpdateFile } from '../@types/updateFile';
import packageJson from '../package.json';
import { CompilerOptions, transpileModule } from 'typescript';
import tsConfig from '../tsconfig.json';
import fsextra from 'fs-extra';
import seiHelper from 'sei-helper';
const workingDir = (process as NodeJS.Process & {
pkg?: unknown
}).pkg ? path.dirname(process.execPath) : path.join(__dirname, '/..');
const updateFilePlace = path.join(workingDir, 'config', 'updates.json');
const updateIgnore = [
'*.d.ts',
'.git',
'lib',
'node_modules',
'@types',
path.join('bin', 'mkvtoolnix'),
path.join('config', 'token.yml'),
'.eslint',
'tsconfig.json',
'updates.json',
'tsc.ts'
];
const askBeforeUpdate = [
'*.yml'
];
enum ApplyType {
DELETE, ADD, UPDATE
}
export type ApplyItem = {
type: ApplyType,
path: string,
content: string
}
export default (async (force = false) => {
const isPackaged = (process as NodeJS.Process & {
pkg?: unknown
}).pkg ? true : false;
if (isPackaged) {
return;
}
let updateFile: UpdateFile|undefined;
if (fs.existsSync(updateFilePlace)) {
updateFile = JSON.parse(fs.readFileSync(updateFilePlace).toString()) as UpdateFile;
if (new Date() < new Date(updateFile.nextCheck) && !force) {
return;
}
}
console.log('Checking for updates...');
const tagRequest = await got('https://api.github.com/repos/anidl/multi-downloader-nx/tags');
const tags = JSON.parse(tagRequest.body) as GithubTag[];
if (tags.length > 0) {
const newer = tags.filter(a => {
return isNewer(packageJson.version, a.name);
});
console.log(`Found ${tags.length} release tags and ${newer.length} that are new.`);
if (newer.length < 1) {
console.log('[INFO] No new tags found');
return done();
}
const newest = newer.sort((a, b) => a.name < b.name ? 1 : a.name > b.name ? -1 : 0)[0];
const compareRequest = await got(`https://api.github.com/repos/anidl/multi-downloader-nx/compare/${packageJson.version}...${newest.name}`);
const compareJSON = JSON.parse(compareRequest.body) as TagCompare;
console.log(`You are behind by ${compareJSON.ahead_by} releases!`);
const changedFiles = compareJSON.files.map(a => ({
...a,
filename: path.join(...a.filename.split('/'))
})).filter(a => {
return !updateIgnore.some(_filter => matchString(_filter, a.filename));
});
if (changedFiles.length < 1) {
console.log('[INFO] No file changes found... updating package.json. If you think this is an error please get the newst version yourself.');
return done(newest.name);
}
console.log(`Found file changes: \n${changedFiles.map(a => ` [${
a.status === 'modified' ? '*' : a.status === 'added' ? '+' : '-'
}] ${a.filename}`).join('\n')}`);
const remove: string[] = [];
changedFiles.filter(a => a.status !== 'added').forEach(async a => {
if (!askBeforeUpdate.some(pattern => matchString(pattern, a.filename)))
return;
const answer = await seiHelper.question(`The developer decided that the file '${a.filename}' may contain information you changed yourself. Should they be overriden to be updated? [y/N]`);
if (answer.toLowerCase() === 'y')
remove.push(a.sha);
});
const changesToApply = await Promise.all(changedFiles.filter(a => !remove.includes(a.sha)).map(async (a): Promise<ApplyItem> => {
if (a.filename.endsWith('.ts')) {
const ret = {
path: a.filename.slice(0, -2) + 'js',
content: transpileModule((await got(a.raw_url)).body, {
compilerOptions: tsConfig.compilerOptions as unknown as CompilerOptions
}).outputText,
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
};
console.log('✓ transpiled %s', ret.path);
return ret;
} else {
const ret = {
path: a.filename,
content: (await got(a.raw_url)).body,
type: a.status === 'modified' ? ApplyType.UPDATE : a.status === 'added' ? ApplyType.ADD : ApplyType.DELETE
};
console.log('✓ transpiled %s', ret.path);
return ret;
}
}));
changesToApply.forEach(a => {
fsextra.ensureDirSync(path.dirname(a.path));
fs.writeFileSync(path.join(__dirname, '..', a.path), a.content);
console.log('✓ written %s', a.path);
});
console.log('[INFO] Done');
return done();
}
});
function done(newVersion?: string) {
const next = new Date(Date.now() + 1000 * 60 * 60 * 24);
fs.writeFileSync(updateFilePlace, JSON.stringify({
lastCheck: Date.now(),
nextCheck: next.getTime()
} as UpdateFile, null, 2));
if (newVersion) {
fs.writeFileSync('../package.json', JSON.stringify({
...packageJson,
version: newVersion
}, null, 4));
}
console.log('[INFO] Searching for update finished. Next time running on the ' + next.toLocaleDateString() + ' at ' + next.toLocaleTimeString() + '.');
}
function isNewer(curr: string, compare: string) : boolean {
const currParts = curr.split('.').map(a => parseInt(a));
const compareParts = compare.split('.').map(a => parseInt(a));
for (let i = 0; i < Math.max(currParts.length, compareParts.length); i++) {
if (currParts.length <= i)
return true;
if (compareParts.length <= i)
return false;
if (currParts[i] !== compareParts[i])
return compareParts[i] > currParts[i];
}
return false;
}
function matchString(pattern: string, toMatch: string) : boolean {
const filter = path.join('..', pattern);
if (pattern.startsWith('*')) {
return toMatch.endsWith(pattern.slice(1));
} else if (filter.split(path.sep).pop()?.indexOf('.') === -1) {
return toMatch.startsWith(filter);
} else {
return toMatch.split(path.sep).pop() === pattern;
}
}

View file

@ -1,174 +1,174 @@
// vtt loader
export type Record = {
text?: string,
time_start?: string,
time_end?: string,
ext_param?: unknown
};
export type NullRecord = Record | null;
function loadVtt(vttStr: string) {
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
const data = []; let lineBuf = [], record: NullRecord = null;
// check lines
for (const l of lines) {
const m = l.match(rx);
if (m) {
if (lineBuf.length > 0) {
lineBuf.pop();
}
if (record !== null) {
record.text = lineBuf.join('\n');
data.push(record);
}
record = {
time_start: m[1],
time_end: m[2],
ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (p[c[0]] = c[1]) && p, {}),
};
lineBuf = [];
continue;
}
lineBuf.push(l);
}
if (record !== null) {
if (lineBuf[lineBuf.length - 1] === '') {
lineBuf.pop();
}
record.text = lineBuf.join('\n');
data.push(record);
}
return data;
}
// ass specific
function convertToAss(vttStr: string, lang: string, fontSize: number, fontName?: string){
let ass = [
'\ufeff[Script Info]',
`Title: ${lang}`,
'ScriptType: v4.00+',
'PlayResX: 1280',
'PlayResY: 720',
'WrapStyle: 0',
'ScaledBorderAndShadow: yes',
'',
'[V4+ Styles]',
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, '
+ 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, '
+ 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
`Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`,
`Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`,
'',
'[Events]',
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
];
const vttData = loadVtt(vttStr);
for (const l of vttData) {
const line = convertToAssLine(l, 'Main');
ass = ass.concat(line);
}
return ass.join('\r\n') + '\r\n';
}
function convertToAssLine(l: Record, style: string) {
const start = convertTime(l.time_start as string);
const end = convertTime(l.time_end as string);
const text = convertToAssText(l.text as string);
// debugger
if ((l.ext_param as any).line === '7%') {
style = 'MainTop';
}
if ((l.ext_param as any).line === '10%') {
style = 'MainTop';
}
return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`;
}
function convertToAssText(text: string) {
text = text
.replace(/\r/g, '')
.replace(/\n/g, '\\N')
.replace(/\\N +/g, '\\N')
.replace(/ +\\N/g, '\\N')
.replace(/(\\N)+/g, '\\N')
.replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}')
.replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}')
.replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/<[^>]>/g, '')
.replace(/\\N$/, '')
.replace(/ +$/, '');
return text;
}
// srt specific
function convertToSrt(vttStr: string){
let srt: string[] = [], srtLineIdx = 0;
const vttData = loadVtt(vttStr);
for (const l of vttData) {
srtLineIdx++;
const line = convertToSrtLine(l, srtLineIdx);
srt = srt.concat(line);
}
return srt.join('\r\n') + '\r\n';
}
function convertToSrtLine(l: Record, idx: number) : string {
const bom = idx == 1 ? '\ufeff' : '';
const start = convertTime(l.time_start as string, true);
const end = convertTime(l.time_end as string, true);
const text = l.text;
return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`;
}
// time parser
function convertTime(time: string, srtFormat = false) {
const mTime = time.match(/([\d:]*)\.?(\d*)/);
if (!mTime){
return srtFormat ? '00:00:00,000' : '0:00:00.00';
}
return toSubsTime(mTime[0], srtFormat);
}
function toSubsTime(str: string, srtFormat: boolean) : string {
const n = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx;
const msLen = srtFormat ? 3 : 2;
const hLen = srtFormat ? 2 : 1;
x[3] = '0.' + ('' + x[3]).padStart(3, '0');
sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]);
sx = sx.toFixed(msLen).split('.');
n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen));
sx = Number(sx[0]);
n.unshift(padTimeNum(':', sx%60, 2));
n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2));
n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen));
return n.join('');
}
function padTimeNum(sep: string, input: string|number , pad:number){
return sep + ('' + input).padStart(pad, '0');
}
// export module
const _default = (vttStr: string, toSrt: boolean, lang = 'English', fontSize: number, fontName?: string) => {
const convert = toSrt ? convertToSrt : convertToAss;
return convert(vttStr, lang, fontSize, fontName);
};
export default _default;
// vtt loader
export type Record = {
text?: string,
time_start?: string,
time_end?: string,
ext_param?: unknown
};
export type NullRecord = Record | null;
function loadVtt(vttStr: string) {
const rx = /^([\d:.]*) --> ([\d:.]*)\s?(.*?)\s*$/;
const lines = vttStr.replace(/\r?\n/g, '\n').split('\n');
const data = []; let lineBuf = [], record: NullRecord = null;
// check lines
for (const l of lines) {
const m = l.match(rx);
if (m) {
if (lineBuf.length > 0) {
lineBuf.pop();
}
if (record !== null) {
record.text = lineBuf.join('\n');
data.push(record);
}
record = {
time_start: m[1],
time_end: m[2],
ext_param: m[3].split(' ').map(x => x.split(':')).reduce((p: any, c: any) => (p[c[0]] = c[1]) && p, {}),
};
lineBuf = [];
continue;
}
lineBuf.push(l);
}
if (record !== null) {
if (lineBuf[lineBuf.length - 1] === '') {
lineBuf.pop();
}
record.text = lineBuf.join('\n');
data.push(record);
}
return data;
}
// ass specific
function convertToAss(vttStr: string, lang: string, fontSize: number, fontName?: string){
let ass = [
'\ufeff[Script Info]',
`Title: ${lang}`,
'ScriptType: v4.00+',
'PlayResX: 1280',
'PlayResY: 720',
'WrapStyle: 0',
'ScaledBorderAndShadow: yes',
'',
'[V4+ Styles]',
'Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, '
+ 'Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, '
+ 'BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
`Style: Main,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,2,10,10,10,1`,
`Style: MainTop,${fontName || 'Noto Sans'},${fontSize},&H00FFFFFF,&H000000FF,&H00020713,&H00000000,0,0,0,0,100,100,0,0,1,3,0,8,10,10,10,1`,
'',
'[Events]',
'Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text',
];
const vttData = loadVtt(vttStr);
for (const l of vttData) {
const line = convertToAssLine(l, 'Main');
ass = ass.concat(line);
}
return ass.join('\r\n') + '\r\n';
}
function convertToAssLine(l: Record, style: string) {
const start = convertTime(l.time_start as string);
const end = convertTime(l.time_end as string);
const text = convertToAssText(l.text as string);
// debugger
if ((l.ext_param as any).line === '7%') {
style = 'MainTop';
}
if ((l.ext_param as any).line === '10%') {
style = 'MainTop';
}
return `Dialogue: 0,${start},${end},${style},,0,0,0,,${text}`;
}
function convertToAssText(text: string) {
text = text
.replace(/\r/g, '')
.replace(/\n/g, '\\N')
.replace(/\\N +/g, '\\N')
.replace(/ +\\N/g, '\\N')
.replace(/(\\N)+/g, '\\N')
.replace(/<b[^>]*>([^<]*)<\/b>/g, '{\\b1}$1{\\b0}')
.replace(/<i[^>]*>([^<]*)<\/i>/g, '{\\i1}$1{\\i0}')
.replace(/<u[^>]*>([^<]*)<\/u>/g, '{\\u1}$1{\\u0}')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.replace(/<[^>]>/g, '')
.replace(/\\N$/, '')
.replace(/ +$/, '');
return text;
}
// srt specific
function convertToSrt(vttStr: string){
let srt: string[] = [], srtLineIdx = 0;
const vttData = loadVtt(vttStr);
for (const l of vttData) {
srtLineIdx++;
const line = convertToSrtLine(l, srtLineIdx);
srt = srt.concat(line);
}
return srt.join('\r\n') + '\r\n';
}
function convertToSrtLine(l: Record, idx: number) : string {
const bom = idx == 1 ? '\ufeff' : '';
const start = convertTime(l.time_start as string, true);
const end = convertTime(l.time_end as string, true);
const text = l.text;
return `${bom}${idx}\r\n${start} --> ${end}\r\n${text}\r\n`;
}
// time parser
function convertTime(time: string, srtFormat = false) {
const mTime = time.match(/([\d:]*)\.?(\d*)/);
if (!mTime){
return srtFormat ? '00:00:00,000' : '0:00:00.00';
}
return toSubsTime(mTime[0], srtFormat);
}
function toSubsTime(str: string, srtFormat: boolean) : string {
const n = [], x: (string|number)[] = str.split(/[:.]/).map(x => Number(x)); let sx;
const msLen = srtFormat ? 3 : 2;
const hLen = srtFormat ? 2 : 1;
x[3] = '0.' + ('' + x[3]).padStart(3, '0');
sx = (x[0] as number)*60*60 + (x[1] as number)*60 + (x[2] as number) + Number(x[3]);
sx = sx.toFixed(msLen).split('.');
n.unshift(padTimeNum((srtFormat ? ',' : '.'), sx[1], msLen));
sx = Number(sx[0]);
n.unshift(padTimeNum(':', sx%60, 2));
n.unshift(padTimeNum(':', Math.floor(sx/60)%60, 2));
n.unshift(padTimeNum('', Math.floor(sx/3600)%60, hLen));
return n.join('');
}
function padTimeNum(sep: string, input: string|number , pad:number){
return sep + ('' + input).padStart(pad, '0');
}
// export module
const _default = (vttStr: string, toSrt: boolean, lang = 'English', fontSize: number, fontName?: string) => {
const convert = toSrt ? convertToSrt : convertToAss;
return convert(vttStr, lang, fontSize, fontName);
};
export default _default;

2650
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "multi-downloader-nx",
"short_name": "aniDL",
"version": "3.0.0",
"version": "3.0.1",
"description": "Download videos from Funimation or Crunchyroll via cli",
"keywords": [
"download",

322
tsc.ts
View file

@ -1,162 +1,162 @@
import { ChildProcess, exec } from 'child_process';
import fs from 'fs-extra';
import path from 'path';
import { removeSync, copyFileSync } from 'fs-extra';
import packageJSON from './package.json';
const argv = process.argv.slice(2);
let buildIgnore: string[] = [];
const isTest = argv.length > 0 && argv[0] === 'test';
const isGUI = !(argv.length > 1 && argv[1] === 'false');
if (!isTest)
buildIgnore = [
'*/\\.env',
'*/node_modules/*'
];
if (!isGUI)
buildIgnore = buildIgnore.concat([
'./gui*',
'./build*'
])
const ignore = [
...buildIgnore,
'*/\\.git*',
'./lib*',
'*/@types*',
'./out*',
'./bin/mkvtoolnix*',
'./config/token.yml$',
'./config/updates.json$',
'./config/cr_token.yml$',
'./config/funi_token.yml$',
'*/\\.eslint*',
'*/*\\.tsx?$',
'./fonts*',
'./gui/react*',
].map(a => a.replace(/\*/g, '[^]*').replace(/\.\//g, escapeRegExp(__dirname) + '/').replace(/\//g, path.sep === '\\' ? '\\\\' : '/')).map(a => new RegExp(a, 'i'));
export { ignore };
(async () => {
const waitForProcess = async (proc: ChildProcess) => {
return new Promise((resolve, reject) => {
proc.stdout?.on('data', console.log);
proc.stderr?.on('data', console.error);
proc.on('close', resolve);
proc.on('error', reject);
});
};
process.stdout.write('Removing lib dir... ');
removeSync('lib');
process.stdout.write('✓\nRunning tsc... ');
const tsc = exec('npx tsc');
await waitForProcess(tsc);
if (!isGUI) {
fs.emptyDirSync(path.join('lib', 'gui'));
fs.rmdirSync(path.join('lib', 'gui'));
}
if (!isTest && isGUI) {
process.stdout.write('✓\nBuilding react... ');
const installReactDependencies = exec('npm install', {
cwd: path.join(__dirname, 'gui', 'react'),
});
await waitForProcess(installReactDependencies);
const react = exec('npm run build', {
cwd: path.join(__dirname, 'gui', 'react'),
});
await waitForProcess(react);
}
process.stdout.write('✓\nCopying files... ');
if (!isTest && isGUI) {
copyDir(path.join(__dirname, 'gui', 'react', 'build'), path.join(__dirname, 'lib', 'gui', 'electron', 'build'));
}
const files = readDir(__dirname);
files.forEach(item => {
const itemPath = path.join(__dirname, 'lib', item.path.replace(__dirname, ''));
if (item.stats.isDirectory()) {
if (!fs.existsSync(itemPath))
fs.mkdirSync(itemPath);
} else {
copyFileSync(item.path, itemPath);
}
});
process.stdout.write('✓\nInstalling dependencies... ');
if (!isTest && !isGUI) {
alterJSON();
}
if (!isTest) {
const dependencies = exec(`npm install ${isGUI ? '' : '--production'}`, {
cwd: path.join(__dirname, 'lib')
});
await waitForProcess(dependencies);
}
process.stdout.write('✓\n');
})();
function alterJSON() {
packageJSON.main = 'index.js';
fs.writeFileSync(path.join('lib', 'package.json'), JSON.stringify(packageJSON, null, 4));
}
function readDir (dir: string): {
path: string,
stats: fs.Stats
}[] {
const items: {
path: string,
stats: fs.Stats
}[] = [];
const content = fs.readdirSync(dir);
itemLoop: for (const item of content) {
const itemPath = path.join(dir, item);
for (const ignoreItem of ignore) {
if (ignoreItem.test(itemPath))
continue itemLoop;
}
const stats = fs.statSync(itemPath);
items.push({
path: itemPath,
stats
});
if (stats.isDirectory()) {
items.push(...readDir(itemPath));
}
}
return items;
}
async function copyDir(src: string, dest: string) {
await fs.promises.mkdir(dest, { recursive: true });
const entries = await fs.promises.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
entry.isDirectory() ?
await copyDir(srcPath, destPath) :
await fs.promises.copyFile(srcPath, destPath);
}
}
function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
import { ChildProcess, exec } from 'child_process';
import fs from 'fs-extra';
import path from 'path';
import { removeSync, copyFileSync } from 'fs-extra';
import packageJSON from './package.json';
const argv = process.argv.slice(2);
let buildIgnore: string[] = [];
const isTest = argv.length > 0 && argv[0] === 'test';
const isGUI = !(argv.length > 1 && argv[1] === 'false');
if (!isTest)
buildIgnore = [
'*/\\.env',
'*/node_modules/*'
];
if (!isGUI)
buildIgnore = buildIgnore.concat([
'./gui*',
'./build*'
]);
const ignore = [
...buildIgnore,
'*/\\.git*',
'./lib*',
'*/@types*',
'./out*',
'./bin/mkvtoolnix*',
'./config/token.yml$',
'./config/updates.json$',
'./config/cr_token.yml$',
'./config/funi_token.yml$',
'*/\\.eslint*',
'*/*\\.tsx?$',
'./fonts*',
'./gui/react*',
].map(a => a.replace(/\*/g, '[^]*').replace(/\.\//g, escapeRegExp(__dirname) + '/').replace(/\//g, path.sep === '\\' ? '\\\\' : '/')).map(a => new RegExp(a, 'i'));
export { ignore };
(async () => {
const waitForProcess = async (proc: ChildProcess) => {
return new Promise((resolve, reject) => {
proc.stdout?.on('data', console.log);
proc.stderr?.on('data', console.error);
proc.on('close', resolve);
proc.on('error', reject);
});
};
process.stdout.write('Removing lib dir... ');
removeSync('lib');
process.stdout.write('✓\nRunning tsc... ');
const tsc = exec('npx tsc');
await waitForProcess(tsc);
if (!isGUI) {
fs.emptyDirSync(path.join('lib', 'gui'));
fs.rmdirSync(path.join('lib', 'gui'));
}
if (!isTest && isGUI) {
process.stdout.write('✓\nBuilding react... ');
const installReactDependencies = exec('npm install', {
cwd: path.join(__dirname, 'gui', 'react'),
});
await waitForProcess(installReactDependencies);
const react = exec('npm run build', {
cwd: path.join(__dirname, 'gui', 'react'),
});
await waitForProcess(react);
}
process.stdout.write('✓\nCopying files... ');
if (!isTest && isGUI) {
copyDir(path.join(__dirname, 'gui', 'react', 'build'), path.join(__dirname, 'lib', 'gui', 'electron', 'build'));
}
const files = readDir(__dirname);
files.forEach(item => {
const itemPath = path.join(__dirname, 'lib', item.path.replace(__dirname, ''));
if (item.stats.isDirectory()) {
if (!fs.existsSync(itemPath))
fs.mkdirSync(itemPath);
} else {
copyFileSync(item.path, itemPath);
}
});
process.stdout.write('✓\nInstalling dependencies... ');
if (!isTest && !isGUI) {
alterJSON();
}
if (!isTest) {
const dependencies = exec(`npm install ${isGUI ? '' : '--production'}`, {
cwd: path.join(__dirname, 'lib')
});
await waitForProcess(dependencies);
}
process.stdout.write('✓\n');
})();
function alterJSON() {
packageJSON.main = 'index.js';
fs.writeFileSync(path.join('lib', 'package.json'), JSON.stringify(packageJSON, null, 4));
}
function readDir (dir: string): {
path: string,
stats: fs.Stats
}[] {
const items: {
path: string,
stats: fs.Stats
}[] = [];
const content = fs.readdirSync(dir);
itemLoop: for (const item of content) {
const itemPath = path.join(dir, item);
for (const ignoreItem of ignore) {
if (ignoreItem.test(itemPath))
continue itemLoop;
}
const stats = fs.statSync(itemPath);
items.push({
path: itemPath,
stats
});
if (stats.isDirectory()) {
items.push(...readDir(itemPath));
}
}
return items;
}
async function copyDir(src: string, dest: string) {
await fs.promises.mkdir(dest, { recursive: true });
const entries = await fs.promises.readdir(src, { withFileTypes: true });
for (const entry of entries) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
entry.isDirectory() ?
await copyDir(srcPath, destPath) :
await fs.promises.copyFile(srcPath, destPath);
}
}
function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}