commit
e5bbc09f25
59 changed files with 11170 additions and 1920 deletions
7
.github/workflows/release-matrix.yml
vendored
7
.github/workflows/release-matrix.yml
vendored
|
|
@ -8,9 +8,10 @@ jobs:
|
||||||
build:
|
build:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
build_type: [ ubuntu, macos, windows ]
|
build_type: [ linux, macos, windows ]
|
||||||
|
build_arch: [ x64 ]
|
||||||
gui: [ gui, cli ]
|
gui: [ gui, cli ]
|
||||||
runs-on: ${{ matrix.build_type }}-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
@ -38,7 +39,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ github.event.release.upload_url }}
|
upload_url: ${{ github.event.release.upload_url }}
|
||||||
asset_name: multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.gui }}.7z
|
asset_name: multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.gui }}.7z
|
||||||
asset_path: ./lib/_builds/multi-downloader-nx-${{ matrix.build_type }}64-${{ matrix.gui }}.7z
|
asset_path: ./lib/_builds/multi-downloader-nx-${{ matrix.build_type }}-${{ matrix.build_arch }}-${{ matrix.gui }}.7z
|
||||||
asset_content_type: application/x-7z-compressed
|
asset_content_type: application/x-7z-compressed
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -24,6 +24,7 @@ cr_token.yml
|
||||||
hd_profile.yml
|
hd_profile.yml
|
||||||
hd_sess.yml
|
hd_sess.yml
|
||||||
hd_token.yml
|
hd_token.yml
|
||||||
|
hd_new_token.yml
|
||||||
archive.json
|
archive.json
|
||||||
guistate.json
|
guistate.json
|
||||||
fonts
|
fonts
|
||||||
|
|
@ -37,4 +38,7 @@ crunchyendpoints
|
||||||
/logs
|
/logs
|
||||||
/tmp/*/
|
/tmp/*/
|
||||||
/videos/*/
|
/videos/*/
|
||||||
/tmp/*.*
|
/tmp/*.*
|
||||||
|
bin
|
||||||
|
widevine/*
|
||||||
|
!widevine/.gitkeep
|
||||||
|
|
|
||||||
56
@types/crunchyAndroidEpisodes.d.ts
vendored
56
@types/crunchyAndroidEpisodes.d.ts
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Images } from './crunchyEpisodeList';
|
||||||
|
|
||||||
export interface CrunchyAndroidEpisodes {
|
export interface CrunchyAndroidEpisodes {
|
||||||
__class__: string;
|
__class__: string;
|
||||||
__href__: string;
|
__href__: string;
|
||||||
|
|
@ -5,13 +7,13 @@ export interface CrunchyAndroidEpisodes {
|
||||||
__links__: Actions;
|
__links__: Actions;
|
||||||
__actions__: Actions;
|
__actions__: Actions;
|
||||||
total: number;
|
total: number;
|
||||||
items: CrunchyEpisode[];
|
items: CrunchyAndroidEpisode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Actions {
|
export interface Actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CrunchyEpisode {
|
export interface CrunchyAndroidEpisode {
|
||||||
__class__: string;
|
__class__: string;
|
||||||
__href__: string;
|
__href__: string;
|
||||||
__resource_key__: string;
|
__resource_key__: string;
|
||||||
|
|
@ -19,7 +21,7 @@ export interface CrunchyEpisode {
|
||||||
__actions__: Actions;
|
__actions__: Actions;
|
||||||
playback: string;
|
playback: string;
|
||||||
id: string;
|
id: string;
|
||||||
channel_id: string;
|
channel_id: ChannelID;
|
||||||
series_id: string;
|
series_id: string;
|
||||||
series_title: string;
|
series_title: string;
|
||||||
series_slug_title: string;
|
series_slug_title: string;
|
||||||
|
|
@ -37,19 +39,19 @@ export interface CrunchyEpisode {
|
||||||
next_episode_id: string;
|
next_episode_id: string;
|
||||||
next_episode_title: string;
|
next_episode_title: string;
|
||||||
hd_flag: boolean;
|
hd_flag: boolean;
|
||||||
maturity_ratings: string[];
|
maturity_ratings: MaturityRating[];
|
||||||
extended_maturity_rating: Actions;
|
extended_maturity_rating: Actions;
|
||||||
is_mature: boolean;
|
is_mature: boolean;
|
||||||
mature_blocked: boolean;
|
mature_blocked: boolean;
|
||||||
episode_air_date: string;
|
episode_air_date: Date;
|
||||||
upload_date: string;
|
upload_date: Date;
|
||||||
availability_starts: string;
|
availability_starts: Date;
|
||||||
availability_ends: string;
|
availability_ends: Date;
|
||||||
eligible_region: string;
|
eligible_region: string;
|
||||||
available_date: Date;
|
available_date: Date;
|
||||||
free_available_date: string;
|
free_available_date: Date;
|
||||||
premium_date: Date;
|
premium_date: Date;
|
||||||
premium_available_date: string;
|
premium_available_date: Date;
|
||||||
is_subbed: boolean;
|
is_subbed: boolean;
|
||||||
is_dubbed: boolean;
|
is_dubbed: boolean;
|
||||||
is_clip: boolean;
|
is_clip: boolean;
|
||||||
|
|
@ -57,13 +59,13 @@ export interface CrunchyEpisode {
|
||||||
seo_description: string;
|
seo_description: string;
|
||||||
season_tags: string[];
|
season_tags: string[];
|
||||||
available_offline: boolean;
|
available_offline: boolean;
|
||||||
subtitle_locales: string[];
|
subtitle_locales: Locale[];
|
||||||
availability_notes: string;
|
availability_notes: string;
|
||||||
audio_locale: string;
|
audio_locale: Locale;
|
||||||
versions: Version[];
|
versions: Version[];
|
||||||
closed_captions_available: boolean;
|
closed_captions_available: boolean;
|
||||||
identifier: string;
|
identifier: string;
|
||||||
media_type: string;
|
media_type: MediaType;
|
||||||
slug: string;
|
slug: string;
|
||||||
images: Images;
|
images: Images;
|
||||||
duration_ms: number;
|
duration_ms: number;
|
||||||
|
|
@ -76,21 +78,17 @@ export interface CrunchyEpisode {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Links {
|
export interface Links {
|
||||||
'episode/channel': EpisodeChannel;
|
'episode/channel': Link;
|
||||||
'episode/next_episode': EpisodeChannel;
|
'episode/next_episode': Link;
|
||||||
'episode/season': EpisodeChannel;
|
'episode/season': Link;
|
||||||
'episode/series': EpisodeChannel;
|
'episode/series': Link;
|
||||||
streams: EpisodeChannel;
|
streams: Link;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EpisodeChannel {
|
export interface Link {
|
||||||
href: string;
|
href: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Images {
|
|
||||||
thumbnail: Array<Thumbnail[]>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Thumbnail {
|
export interface Thumbnail {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
|
@ -117,6 +115,18 @@ export enum Locale {
|
||||||
jaJP = 'ja-JP',
|
jaJP = 'ja-JP',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum MediaType {
|
||||||
|
Episode = 'episode',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChannelID {
|
||||||
|
Crunchyroll = 'crunchyroll',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MaturityRating {
|
||||||
|
Tv14 = 'TV-14',
|
||||||
|
}
|
||||||
|
|
||||||
export interface Version {
|
export interface Version {
|
||||||
audio_locale: Locale;
|
audio_locale: Locale;
|
||||||
guid: string;
|
guid: string;
|
||||||
|
|
|
||||||
55
@types/crunchyAndroidStreams.d.ts
vendored
55
@types/crunchyAndroidStreams.d.ts
vendored
|
|
@ -3,18 +3,35 @@ export interface CrunchyAndroidStreams {
|
||||||
__href__: string;
|
__href__: string;
|
||||||
__resource_key__: string;
|
__resource_key__: string;
|
||||||
__links__: Links;
|
__links__: Links;
|
||||||
__actions__: Actions;
|
__actions__: Record<unknown, unknown>;
|
||||||
media_id: string;
|
media_id: string;
|
||||||
audio_locale: string;
|
audio_locale: Locale;
|
||||||
subtitles: { [key: string]: Subtitle };
|
subtitles: Subtitles;
|
||||||
closed_captions: Actions;
|
closed_captions: Subtitles;
|
||||||
streams: Streams;
|
streams: Streams;
|
||||||
bifs: string[];
|
bifs: string[];
|
||||||
versions: Version[];
|
versions: Version[];
|
||||||
captions: Actions;
|
captions: Record<unknown, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Actions {
|
export interface Subtitles {
|
||||||
|
'': Subtitle;
|
||||||
|
'en-US'?: Subtitle;
|
||||||
|
'es-LA'?: Subtitle;
|
||||||
|
'es-419'?: Subtitle;
|
||||||
|
'es-ES'?: Subtitle;
|
||||||
|
'pt-BR'?: Subtitle;
|
||||||
|
'fr-FR'?: Subtitle;
|
||||||
|
'de-DE'?: Subtitle;
|
||||||
|
'ar-ME'?: Subtitle;
|
||||||
|
'ar-SA'?: Subtitle;
|
||||||
|
'it-IT'?: Subtitle;
|
||||||
|
'ru-RU'?: Subtitle;
|
||||||
|
'tr-TR'?: Subtitle;
|
||||||
|
'hi-IN'?: Subtitle;
|
||||||
|
'zh-CN'?: Subtitle;
|
||||||
|
'ko-KR'?: Subtitle;
|
||||||
|
'ja-JP'?: Subtitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Links {
|
export interface Links {
|
||||||
|
|
@ -30,7 +47,7 @@ export interface Streams {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Download {
|
export interface Download {
|
||||||
hardsub_locale: string;
|
hardsub_locale: Locale;
|
||||||
hardsub_lang?: string;
|
hardsub_lang?: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
@ -40,13 +57,13 @@ export interface Urls {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Subtitle {
|
export interface Subtitle {
|
||||||
locale: string;
|
locale: Locale;
|
||||||
url: string;
|
url: string;
|
||||||
format: string;
|
format: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Version {
|
export interface Version {
|
||||||
audio_locale: string;
|
audio_locale: Locale;
|
||||||
guid: string;
|
guid: string;
|
||||||
original: boolean;
|
original: boolean;
|
||||||
variant: string;
|
variant: string;
|
||||||
|
|
@ -54,3 +71,23 @@ export interface Version {
|
||||||
media_guid: string;
|
media_guid: string;
|
||||||
is_premium_only: boolean;
|
is_premium_only: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum Locale {
|
||||||
|
default = '',
|
||||||
|
enUS = 'en-US',
|
||||||
|
esLA = 'es-LA',
|
||||||
|
es419 = 'es-419',
|
||||||
|
esES = 'es-ES',
|
||||||
|
ptBR = 'pt-BR',
|
||||||
|
frFR = 'fr-FR',
|
||||||
|
deDE = 'de-DE',
|
||||||
|
arME = 'ar-ME',
|
||||||
|
arSA = 'ar-SA',
|
||||||
|
itIT = 'it-IT',
|
||||||
|
ruRU = 'ru-RU',
|
||||||
|
trTR = 'tr-TR',
|
||||||
|
hiIN = 'hi-IN',
|
||||||
|
zhCN = 'zh-CN',
|
||||||
|
koKR = 'ko-KR',
|
||||||
|
jaJP = 'ja-JP',
|
||||||
|
}
|
||||||
26
@types/crunchyChapters.d.ts
vendored
Normal file
26
@types/crunchyChapters.d.ts
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
export interface CrunchyChapters {
|
||||||
|
[key: string]: CrunchyChapter;
|
||||||
|
lastUpdate: Date;
|
||||||
|
mediaId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrunchyChapter {
|
||||||
|
approverId: string;
|
||||||
|
distributionNumber: string;
|
||||||
|
end: number;
|
||||||
|
start: number;
|
||||||
|
title: string;
|
||||||
|
seriesId: string;
|
||||||
|
new: boolean;
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CrunchyOldChapter {
|
||||||
|
media_id: string;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
duration: number;
|
||||||
|
comparedWith: string;
|
||||||
|
ordering: string;
|
||||||
|
last_updated: Date;
|
||||||
|
}
|
||||||
19
@types/crunchyEpisodeList.d.ts
vendored
19
@types/crunchyEpisodeList.d.ts
vendored
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Links } from './crunchyAndroidEpisodes';
|
||||||
|
|
||||||
export interface CrunchyEpisodeList {
|
export interface CrunchyEpisodeList {
|
||||||
total: number;
|
total: number;
|
||||||
data: CrunchyEpisode[];
|
data: CrunchyEpisode[];
|
||||||
|
|
@ -41,27 +43,28 @@ export interface CrunchyEpisode {
|
||||||
listing_id: string;
|
listing_id: string;
|
||||||
episode_air_date: Date;
|
episode_air_date: Date;
|
||||||
slug: string;
|
slug: string;
|
||||||
available_date: null;
|
available_date: Date;
|
||||||
subtitle_locales: Locale[];
|
subtitle_locales: Locale[];
|
||||||
slug_title: string;
|
slug_title: string;
|
||||||
available_offline: boolean;
|
available_offline: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
is_subbed: boolean;
|
is_subbed: boolean;
|
||||||
premium_date: null;
|
premium_date: Date;
|
||||||
upload_date: Date;
|
upload_date: Date;
|
||||||
season_slug_title: string;
|
season_slug_title: string;
|
||||||
closed_captions_available: boolean;
|
closed_captions_available: boolean;
|
||||||
episode_number: number;
|
episode_number: number;
|
||||||
season_tags: any[];
|
season_tags: any[];
|
||||||
maturity_ratings: MaturityRating[];
|
maturity_ratings: MaturityRating[];
|
||||||
streams_link: string;
|
streams_link?: string;
|
||||||
mature_blocked: boolean;
|
mature_blocked: boolean;
|
||||||
is_clip: boolean;
|
is_clip: boolean;
|
||||||
hd_flag: boolean;
|
hd_flag: boolean;
|
||||||
hide_season_title?: boolean;
|
hide_season_title?: boolean;
|
||||||
hide_season_number?: boolean;
|
hide_season_number?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
seq_id: string;
|
seq_id: string;
|
||||||
|
__links__?: Links;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Locale {
|
export enum Locale {
|
||||||
|
|
@ -127,5 +130,5 @@ export interface Version {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Meta {
|
export interface Meta {
|
||||||
versions_considered: boolean;
|
versions_considered?: boolean;
|
||||||
}
|
}
|
||||||
17
@types/crunchyTypes.d.ts
vendored
17
@types/crunchyTypes.d.ts
vendored
|
|
@ -31,9 +31,14 @@ export type CrunchyDownloadOptions = {
|
||||||
dlVideoOnce: boolean,
|
dlVideoOnce: boolean,
|
||||||
skipmux?: boolean,
|
skipmux?: boolean,
|
||||||
syncTiming: boolean,
|
syncTiming: boolean,
|
||||||
|
nocleanup: boolean,
|
||||||
|
chapters: boolean,
|
||||||
|
fontName: string | undefined,
|
||||||
|
fontSize: number,
|
||||||
|
dubLang: string[],
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CurnchyMultiDownload = {
|
export type CrunchyMultiDownload = {
|
||||||
dubLang: string[],
|
dubLang: string[],
|
||||||
all?: boolean,
|
all?: boolean,
|
||||||
but?: boolean,
|
but?: boolean,
|
||||||
|
|
@ -82,8 +87,18 @@ export type DownloadedMedia = {
|
||||||
lang: LanguageItem,
|
lang: LanguageItem,
|
||||||
path: string,
|
path: string,
|
||||||
isPrimary?: boolean
|
isPrimary?: boolean
|
||||||
|
} | {
|
||||||
|
type: 'Audio',
|
||||||
|
lang: LanguageItem,
|
||||||
|
path: string,
|
||||||
|
isPrimary?: boolean
|
||||||
|
} | {
|
||||||
|
type: 'Chapters',
|
||||||
|
lang: LanguageItem,
|
||||||
|
path: string
|
||||||
} | ({
|
} | ({
|
||||||
type: 'Subtitle',
|
type: 'Subtitle',
|
||||||
|
signs: boolean,
|
||||||
cc: boolean
|
cc: boolean
|
||||||
} & sxItem )
|
} & sxItem )
|
||||||
|
|
||||||
|
|
|
||||||
6
@types/messageHandler.d.ts
vendored
6
@types/messageHandler.d.ts
vendored
|
|
@ -40,7 +40,7 @@ export type QueueItem = {
|
||||||
q: number,
|
q: number,
|
||||||
dlVideoOnce: boolean,
|
dlVideoOnce: boolean,
|
||||||
dubLang: string[],
|
dubLang: string[],
|
||||||
image: string
|
image: string,
|
||||||
} & ResolveItemsData
|
} & ResolveItemsData
|
||||||
|
|
||||||
export type ResolveItemsData = {
|
export type ResolveItemsData = {
|
||||||
|
|
@ -106,7 +106,9 @@ export type FuniStreamData = { force?: 'Y'|'y'|'N'|'n'|'C'|'c', callbackMaker?:
|
||||||
forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean, override: string[], videoTitle: string,
|
forceMuxer: AvailableMuxer | undefined, simul: boolean, skipSubMux: boolean, nocleanup: boolean, override: string[], videoTitle: string,
|
||||||
ffmpegOptions: string[], mkvmergeOptions: string[], defaultAudio: LanguageItem, defaultSub: LanguageItem, ccTag: string }
|
ffmpegOptions: string[], mkvmergeOptions: string[], defaultAudio: LanguageItem, defaultSub: LanguageItem, ccTag: string }
|
||||||
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[], ccTag: string }
|
export type FuniSubsData = { nosubs?: boolean, sub: boolean, dlsubs: string[], ccTag: string }
|
||||||
export type DownloadData = { id: string, e: string, dubLang: string[], dlsubs: string[], fileName: string, q: number, novids: boolean, noaudio: boolean, dlVideoOnce: boolean }
|
export type DownloadData = {
|
||||||
|
hslang?: string; id: string, e: string, dubLang: string[], dlsubs: string[], fileName: string, q: number, novids: boolean, noaudio: boolean, dlVideoOnce: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type AuthResponse = ResponseBase<undefined>;
|
export type AuthResponse = ResponseBase<undefined>;
|
||||||
export type FuniSearchReponse = ResponseBase<FunimationSearch>;
|
export type FuniSearchReponse = ResponseBase<FunimationSearch>;
|
||||||
|
|
|
||||||
71
@types/mpd-parser.d.ts
vendored
Normal file
71
@types/mpd-parser.d.ts
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
declare module 'mpd-parser' {
|
||||||
|
export type Segment = {
|
||||||
|
uri: string,
|
||||||
|
timeline: number,
|
||||||
|
duration: number,
|
||||||
|
resolvedUri: string,
|
||||||
|
map: {
|
||||||
|
uri: string,
|
||||||
|
resolvedUri: string,
|
||||||
|
},
|
||||||
|
number: number,
|
||||||
|
presentationTime: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Playlist = {
|
||||||
|
attributes: {
|
||||||
|
NAME: string,
|
||||||
|
BANDWIDTH: number,
|
||||||
|
CODECS: string,
|
||||||
|
'PROGRAM-ID': number,
|
||||||
|
// Following for video only
|
||||||
|
'FRAME-RATE'?: number,
|
||||||
|
AUDIO?: string, // audio stream name
|
||||||
|
SUBTITLES?: string,
|
||||||
|
RESOLUTION?: {
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
uri: string,
|
||||||
|
endList: boolean,
|
||||||
|
timeline: number,
|
||||||
|
resolvedUri: string,
|
||||||
|
targetDuration: number,
|
||||||
|
discontinuitySequence: number,
|
||||||
|
discontinuityStarts: [],
|
||||||
|
timelineStarts: {
|
||||||
|
start: number,
|
||||||
|
timeline: number
|
||||||
|
}[],
|
||||||
|
mediaSequence: number,
|
||||||
|
contentProtection?: {
|
||||||
|
[type: string]: {
|
||||||
|
pssh?: Uint8Array
|
||||||
|
}
|
||||||
|
}
|
||||||
|
segments: Segment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Manifest = {
|
||||||
|
allowCache: boolean,
|
||||||
|
discontinuityStarts: [],
|
||||||
|
segments: [],
|
||||||
|
endList: true,
|
||||||
|
duration: number,
|
||||||
|
playlists: Playlist[],
|
||||||
|
mediaGroups: {
|
||||||
|
AUDIO: {
|
||||||
|
audio: {
|
||||||
|
[name: string]: {
|
||||||
|
language: string,
|
||||||
|
autoselect: boolean,
|
||||||
|
default: boolean,
|
||||||
|
playlists: Playlist[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export function parse(manifest: string): Manifest
|
||||||
|
}
|
||||||
43
@types/newHidiveEpisode.d.ts
vendored
Normal file
43
@types/newHidiveEpisode.d.ts
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
export interface NewHidiveEpisode {
|
||||||
|
description: string;
|
||||||
|
duration: number;
|
||||||
|
title: string;
|
||||||
|
categories: string[];
|
||||||
|
contentDownload: ContentDownload;
|
||||||
|
favourite: boolean;
|
||||||
|
subEvents: any[];
|
||||||
|
thumbnailUrl: string;
|
||||||
|
longDescription: string;
|
||||||
|
posterUrl: string;
|
||||||
|
offlinePlaybackLanguages: string[];
|
||||||
|
externalAssetId: string;
|
||||||
|
maxHeight: number;
|
||||||
|
rating: Rating;
|
||||||
|
episodeInformation: EpisodeInformation;
|
||||||
|
id: number;
|
||||||
|
accessLevel: string;
|
||||||
|
playerUrlCallback: string;
|
||||||
|
thumbnailsPreview: string;
|
||||||
|
displayableTags: any[];
|
||||||
|
plugins: any[];
|
||||||
|
watchStatus: string;
|
||||||
|
computedReleases: any[];
|
||||||
|
licences: any[];
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentDownload {
|
||||||
|
permission: string;
|
||||||
|
period: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeInformation {
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeNumber: number;
|
||||||
|
season: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rating {
|
||||||
|
rating: string;
|
||||||
|
descriptors: any[];
|
||||||
|
}
|
||||||
33
@types/newHidivePlayback.d.ts
vendored
Normal file
33
@types/newHidivePlayback.d.ts
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
export interface NewHidivePlayback {
|
||||||
|
watermark: null;
|
||||||
|
skipMarkers: any[];
|
||||||
|
annotations: null;
|
||||||
|
dash: Format[];
|
||||||
|
hls: Format[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Format {
|
||||||
|
subtitles: Subtitle[];
|
||||||
|
url: string;
|
||||||
|
drm: DRM;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DRM {
|
||||||
|
encryptionMode: string;
|
||||||
|
containerType: string;
|
||||||
|
jwtToken: string;
|
||||||
|
url: string;
|
||||||
|
keySystems: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Subtitle {
|
||||||
|
format: Formats;
|
||||||
|
language: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Formats {
|
||||||
|
Scc = 'scc',
|
||||||
|
Srt = 'srt',
|
||||||
|
Vtt = 'vtt',
|
||||||
|
}
|
||||||
91
@types/newHidiveSearch.d.ts
vendored
Normal file
91
@types/newHidiveSearch.d.ts
vendored
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
export interface NewHidiveSearch {
|
||||||
|
results: Result[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
hits: Hit[];
|
||||||
|
nbHits: number;
|
||||||
|
page: number;
|
||||||
|
nbPages: number;
|
||||||
|
hitsPerPage: number;
|
||||||
|
exhaustiveNbHits: boolean;
|
||||||
|
exhaustiveTypo: boolean;
|
||||||
|
exhaustive: Exhaustive;
|
||||||
|
query: string;
|
||||||
|
params: string;
|
||||||
|
index: string;
|
||||||
|
renderingContent: RenderingContent;
|
||||||
|
processingTimeMS: number;
|
||||||
|
processingTimingsMS: ProcessingTimingsMS;
|
||||||
|
serverTimeMS: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Exhaustive {
|
||||||
|
nbHits: boolean;
|
||||||
|
typo: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Hit {
|
||||||
|
type: string;
|
||||||
|
weight: number;
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
meta: RenderingContent;
|
||||||
|
coverUrl: string;
|
||||||
|
smallCoverUrl: string;
|
||||||
|
seasonsCount: number;
|
||||||
|
tags: string[];
|
||||||
|
localisations: HitLocalisations;
|
||||||
|
ratings: Ratings;
|
||||||
|
objectID: string;
|
||||||
|
_highlightResult: HighlightResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HighlightResult {
|
||||||
|
name: Description;
|
||||||
|
description: Description;
|
||||||
|
tags: Description[];
|
||||||
|
localisations: HighlightResultLocalisations;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Description {
|
||||||
|
value: string;
|
||||||
|
matchLevel: string;
|
||||||
|
matchedWords: string[];
|
||||||
|
fullyHighlighted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HighlightResultLocalisations {
|
||||||
|
en_US: PurpleEnUS;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PurpleEnUS {
|
||||||
|
title: Description;
|
||||||
|
description: Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HitLocalisations {
|
||||||
|
[language: string]: HitLocalization;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HitLocalization {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderingContent {
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Ratings {
|
||||||
|
US: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessingTimingsMS {
|
||||||
|
_request: Request;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Request {
|
||||||
|
queue: number;
|
||||||
|
roundTrip: number;
|
||||||
|
}
|
||||||
89
@types/newHidiveSeason.d.ts
vendored
Normal file
89
@types/newHidiveSeason.d.ts
vendored
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
export interface NewHidiveSeason {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
longDescription: string;
|
||||||
|
smallCoverUrl: string;
|
||||||
|
coverUrl: string;
|
||||||
|
titleUrl: string;
|
||||||
|
posterUrl: string;
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeCount: number;
|
||||||
|
displayableTags: any[];
|
||||||
|
rating: Rating;
|
||||||
|
contentRating: Rating;
|
||||||
|
id: number;
|
||||||
|
series: Series;
|
||||||
|
episodes: Episode[];
|
||||||
|
paging: Paging;
|
||||||
|
licences: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rating {
|
||||||
|
rating: string;
|
||||||
|
descriptors: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Episode {
|
||||||
|
accessLevel: string;
|
||||||
|
availablePurchases?: any[];
|
||||||
|
licenceIds?: any[];
|
||||||
|
type: string;
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
posterUrl: string;
|
||||||
|
duration: number;
|
||||||
|
favourite: boolean;
|
||||||
|
contentDownload: ContentDownload;
|
||||||
|
offlinePlaybackLanguages: string[];
|
||||||
|
externalAssetId: string;
|
||||||
|
subEvents: any[];
|
||||||
|
maxHeight: number;
|
||||||
|
thumbnailsPreview: string;
|
||||||
|
longDescription: string;
|
||||||
|
episodeInformation: EpisodeInformation;
|
||||||
|
categories: string[];
|
||||||
|
displayableTags: any[];
|
||||||
|
watchStatus: string;
|
||||||
|
computedReleases: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentDownload {
|
||||||
|
permission: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeInformation {
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeNumber: number;
|
||||||
|
season: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Paging {
|
||||||
|
moreDataAvailable: boolean;
|
||||||
|
lastSeen: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Series {
|
||||||
|
seriesId: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
longDescription: string;
|
||||||
|
displayableTags: any[];
|
||||||
|
rating: Rating;
|
||||||
|
contentRating: Rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewHidiveSeriesExtra extends Series {
|
||||||
|
season: NewHidiveSeason;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NewHidiveEpisodeExtra extends Episode {
|
||||||
|
titleId: number;
|
||||||
|
nameLong: string;
|
||||||
|
seasonTitle: string;
|
||||||
|
seriesTitle: string;
|
||||||
|
seriesId?: number;
|
||||||
|
isSelected: boolean;
|
||||||
|
jwtToken?: string;
|
||||||
|
}
|
||||||
35
@types/newHidiveSeries.d.ts
vendored
Normal file
35
@types/newHidiveSeries.d.ts
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
export interface NewHidiveSeries {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
longDescription: string;
|
||||||
|
smallCoverUrl: string;
|
||||||
|
coverUrl: string;
|
||||||
|
titleUrl: string;
|
||||||
|
posterUrl: string;
|
||||||
|
seasons: Season[];
|
||||||
|
rating: Rating;
|
||||||
|
contentRating: Rating;
|
||||||
|
displayableTags: any[];
|
||||||
|
paging: Paging;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Rating {
|
||||||
|
rating: string;
|
||||||
|
descriptors: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Paging {
|
||||||
|
moreDataAvailable: boolean;
|
||||||
|
lastSeen: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Season {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
longDescription: string;
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeCount: number;
|
||||||
|
displayableTags: any[];
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
4
@types/playbackData.d.ts
vendored
4
@types/playbackData.d.ts
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
// Generated by https://quicktype.io
|
// Generated by https://quicktype.io
|
||||||
export interface PlaybackData {
|
export interface PlaybackData {
|
||||||
total: number;
|
total: number;
|
||||||
data: { [key: string]: { [key: string]: StreamDetails } };
|
data: [{ [key: string]: { [key: string]: StreamDetails } }];
|
||||||
meta: Meta;
|
meta: Meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +56,7 @@ export interface Meta {
|
||||||
bifs: string[];
|
bifs: string[];
|
||||||
versions: Version[];
|
versions: Version[];
|
||||||
audio_locale: Locale;
|
audio_locale: Locale;
|
||||||
closed_captions: Record<unknown>;
|
closed_captions: Subtitles;
|
||||||
captions: Record<unknown>;
|
captions: Record<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,13 @@ RUN echo 'ffmpeg: "./bin/ffmpeg/ffmpeg"\nmkvmerge: "./bin/mkvtoolnix/mkvmerge"'
|
||||||
|
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm
|
||||||
RUN pnpm i
|
RUN pnpm i
|
||||||
RUN pnpm run build-ubuntu-gui
|
RUN pnpm run build-linux-gui
|
||||||
|
|
||||||
# Move build to new Clean Image
|
# Move build to new Clean Image
|
||||||
|
|
||||||
FROM node
|
FROM node
|
||||||
WORKDIR "/app"
|
WORKDIR "/app"
|
||||||
COPY --from=builder /app/lib/_builds/multi-downloader-nx-ubuntu64-gui ./
|
COPY --from=builder /app/lib/_builds/multi-downloader-nx-linux-x64-gui ./
|
||||||
|
|
||||||
# Install mkvmerge and ffmpeg
|
# Install mkvmerge and ffmpeg
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
ffmpeg: "ffmpeg.exe"
|
ffmpeg: "ffmpeg.exe"
|
||||||
mkvmerge: "mkvmerge.exe"
|
mkvmerge: "mkvmerge.exe"
|
||||||
ffprobe: "ffprobe.exe"
|
ffprobe: "ffprobe.exe"
|
||||||
|
mp4decrypt: "mp4decrypt.exe"
|
||||||
|
|
|
||||||
1067
crunchy.ts
1067
crunchy.ts
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
||||||
# multi-downloader-nx (4.4.4v)
|
# multi-downloader-nx (4.5.1v)
|
||||||
|
|
||||||
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
If you find any bugs in this documentation or in the programm itself please report it [over on GitHub](https://github.com/anidl/multi-downloader-nx/issues).
|
||||||
|
|
||||||
|
|
@ -98,8 +98,7 @@ Get video list by Movie Listing ID
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
| Crunchyroll | `--series ${ID}` | `string` | `No`| `--srz` | `NaN` |
|
| Crunchyroll | `--series ${ID}` | `string` | `No`| `--srz` | `NaN` |
|
||||||
|
|
||||||
This command is used only for crunchyroll.
|
Requested is the ID of a show not a season.
|
||||||
Requested is the ID of a show not a season.
|
|
||||||
#### `-s`
|
#### `-s`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | ---|
|
||||||
|
|
@ -136,6 +135,27 @@ If selected, the best selected quality will be downloaded only for the first lan
|
||||||
then the worst video quality with the same audio quality will be downloaded for every other language.
|
then the worst video quality with the same audio quality will be downloaded for every other language.
|
||||||
By the later merge of the videos, no quality difference will be present.
|
By the later merge of the videos, no quality difference will be present.
|
||||||
This will speed up the download speed, if multiple languages are selected.
|
This will speed up the download speed, if multiple languages are selected.
|
||||||
|
#### `--chapters`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll | `--chapters ` | `boolean` | `No`| `NaN` | `false`| `chapters: ` |
|
||||||
|
|
||||||
|
Will fetch the chapters and add them into the final video.
|
||||||
|
Currently only works with mkvmerge.
|
||||||
|
#### `--crapi`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Crunchyroll | `--crapi ` | `string` | `No`| `NaN` | [`android`, `web`] | `android`| `crapi: ` |
|
||||||
|
|
||||||
|
If set to Android, it has lower quality, but Non-DRM streams,
|
||||||
|
If set to Web, it has a higher quality adaptive stream, but everything is DRM.
|
||||||
|
#### `--hdapi`
|
||||||
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Choices** | **Default** |**cli-default Entry**
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
| Hidive | `--hdapi ` | `string` | `No`| `NaN` | [`old`, `new`] | `old`| `hdapi: ` |
|
||||||
|
|
||||||
|
If set to Old, it has lower quality, but Non-DRM streams, but some people can't use it,
|
||||||
|
If set to New, it has a higher quality stream, but everything is DRM.
|
||||||
#### `--removeBumpers`
|
#### `--removeBumpers`
|
||||||
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
| **Service** | **Usage** | **Type** | **Required** | **Alias** | **Default** |**cli-default Entry**
|
||||||
| --- | --- | --- | --- | --- | --- | ---|
|
| --- | --- | --- | --- | --- | --- | ---|
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,8 @@ This downloader can download anime from different sites. Currently supported are
|
||||||
|
|
||||||
This application is not endorsed by or affiliated with *Funimation*, *Crunchyroll*, or *Hidive*. 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.
|
This application is not endorsed by or affiliated with *Funimation*, *Crunchyroll*, or *Hidive*. 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.
|
||||||
|
|
||||||
## Prerequisites
|
## Dependencies
|
||||||
|
|
||||||
* NodeJS >= 14.6.0 (https://nodejs.org/)
|
|
||||||
* NPM >= 6.9.0 (https://www.npmjs.org/)
|
|
||||||
* PNPM >= 7.0.0 (https://pnpm.io/)
|
|
||||||
* ffmpeg >= 4.0.0 (https://www.videohelp.com/software/ffmpeg)
|
* ffmpeg >= 4.0.0 (https://www.videohelp.com/software/ffmpeg)
|
||||||
* MKVToolNix >= 60.0.0 (https://www.videohelp.com/software/MKVToolNix)
|
* MKVToolNix >= 60.0.0 (https://www.videohelp.com/software/MKVToolNix)
|
||||||
|
|
||||||
|
|
@ -23,22 +20,53 @@ By default this application uses the following paths to programs (main executabl
|
||||||
* `ffmpeg.exe` (From PATH)
|
* `ffmpeg.exe` (From PATH)
|
||||||
* `ffprobe.exe` (From PATH)
|
* `ffprobe.exe` (From PATH)
|
||||||
* `mkvmerge.exe` (From PATH)
|
* `mkvmerge.exe` (From PATH)
|
||||||
|
* `mp4decrypt.exe` (From PATH)
|
||||||
|
|
||||||
To change these paths you need to edit `bin-path.yml` in `./config/` directory.
|
To change these paths you need to edit `bin-path.yml` in `./config/` directory.
|
||||||
|
|
||||||
### Node Modules
|
## CLI Information
|
||||||
|
|
||||||
After installing NodeJS with NPM go to directory with `package.json` file and type: `npm i`. Afterwards run `npm run tsc`. You can now find a lib folder containing the js code execute.
|
See [the documentation](https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md) for a complete list of what options are available. You can define defaults for the commands by editing the `cli-defaults.yml` file in the `./config/` directory.
|
||||||
|
|
||||||
* [check dependencies](https://david-dm.org/anidl/funimation-downloader-nx)
|
### Example usage
|
||||||
|
|
||||||
## CLI Options
|
#### Logging in
|
||||||
|
|
||||||
See [the documentation](https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md)
|
Most services require you to be logged in, in order to download from, an example of how you would login is:
|
||||||
|
|
||||||
## Build instructions
|
```shell
|
||||||
|
AniDL --service {ServiceName} --auth
|
||||||
|
```
|
||||||
|
|
||||||
Please note that nodejs, npm, and pnpm must be installed in your system. For instructions on how to install pnpm, check (https://pnpm.io/installation)
|
#### Searching
|
||||||
|
|
||||||
|
In order to find the IDs to download, you can search from each service by using the `--search` flag like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
AniDL --service {ServiceName} --search {SearchTerm}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Downloading
|
||||||
|
|
||||||
|
Once you have the ID which you can obtain from using the search or other means, you are ready to download, which you can do like this:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
AniDL --service {ServiceName} -s {SeasonID} -e {EpisodeNumber}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Building and running from source
|
||||||
|
|
||||||
|
### Build Dependencies
|
||||||
|
|
||||||
|
Dependencies that are only required for running from code. These are not required if you are using the prebuilt binaries.
|
||||||
|
|
||||||
|
* NodeJS >= 14.6.0 (https://nodejs.org/)
|
||||||
|
* NPM >= 6.9.0 (https://www.npmjs.org/)
|
||||||
|
* PNPM >= 7.0.0 (https://pnpm.io/)
|
||||||
|
|
||||||
|
### Build Instructions
|
||||||
|
|
||||||
|
Please note that NodeJS, NPM, and PNPM must be installed on your system. For instructions on how to install pnpm, check (https://pnpm.io/installation)
|
||||||
|
|
||||||
First clone this repo `git clone https://github.com/anidl/multi-downloader-nx.git`.
|
First clone this repo `git clone https://github.com/anidl/multi-downloader-nx.git`.
|
||||||
|
|
||||||
|
|
@ -47,4 +75,14 @@ Afterwards run `pnpm run tsc false [true if you want gui, false otherwise]`.
|
||||||
|
|
||||||
If you want the `js` files you are done. Just `cd` into the `lib` folder, and run `node index.js --help` to get started with the CLI, or run `node gui.js` to run the GUI
|
If you want the `js` files you are done. Just `cd` into the `lib` folder, and run `node index.js --help` to get started with the CLI, or run `node gui.js` to run the GUI
|
||||||
|
|
||||||
If you want to package the application, run pnpm run build-`{platform}`-`{type}` where `{platform}` is the operating system (currently the choices are windows, ubuntu, macos, and arm) and `{type}` is cli or gui.
|
If you want to package the application, run pnpm run build-`{platform}`-`{type}` where `{platform}` is the operating system (currently the choices are windows, linux, macos, alpine, android, and arm) and `{type}` is cli or gui.
|
||||||
|
|
||||||
|
## DRM Decryption
|
||||||
|
|
||||||
|
### Decryption Requirements
|
||||||
|
|
||||||
|
* mp4decrypt >= Any (http://www.bento4.com/) - Only required for decrypting
|
||||||
|
|
||||||
|
### Instructions
|
||||||
|
|
||||||
|
In order to decrypt DRM content, you will need to have a dumped CDM, after that you will need to place the CDM files (`device_client_id_blob` and `device_private_key`) into the `./widevine/` directory. For legal reasons we do not include the CDM with the software, and you will have to source one yourself.
|
||||||
|
|
|
||||||
BIN
gui/react/public/favicon.webp
Normal file
BIN
gui/react/public/favicon.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
|
|
@ -1,6 +1,8 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<title>Multi Downloader</title>
|
||||||
|
<link rel="icon" type="image/webp" href="favicon.webp">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
|
|
|
||||||
|
|
@ -13,15 +13,21 @@ const Layout: React.FC = () => {
|
||||||
|
|
||||||
const messageHandler = React.useContext(messageChannelContext);
|
const messageHandler = React.useContext(messageChannelContext);
|
||||||
|
|
||||||
return <Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
return <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '100%', alignItems: 'center',}}>
|
||||||
<MenuBar />
|
<MenuBar />
|
||||||
<Box sx={{ height: 50, mb: 4, display: 'flex', gap: 1, mt: 3 }}>
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
width: '93vw',
|
||||||
|
maxWidth: '93rem',
|
||||||
|
maxHeight: '3rem'
|
||||||
|
//backgroundColor: '#ffffff',
|
||||||
|
}}>
|
||||||
<LogoutButton />
|
<LogoutButton />
|
||||||
<AuthButton />
|
<AuthButton />
|
||||||
<Box sx={{ display: 'flex', gap: 1, height: 36 }}>
|
<Button variant="contained" startIcon={<Folder />} onClick={() => messageHandler?.openFolder('content')} sx={{ height: '37px' }}>Open Output Directory</Button>
|
||||||
<Button variant="contained" startIcon={<Folder />} onClick={() => messageHandler?.openFolder('content')}>Open Output Directory</Button>
|
<Button variant="contained" startIcon={<ClearAll />} onClick={() => messageHandler?.clearQueue() } sx={{ height: '37px' }}>Clear Queue</Button>
|
||||||
<Button variant="contained" startIcon={<ClearAll />} onClick={() => messageHandler?.clearQueue() }>Clear Queue</Button>
|
|
||||||
</Box>
|
|
||||||
<AddToQueue />
|
<AddToQueue />
|
||||||
<StartQueueButton />
|
<StartQueueButton />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,8 @@ const makeTheme = (mode: 'dark'|'light') : Partial<Theme> => {
|
||||||
|
|
||||||
const Style: FCWithChildren = ({children}) => {
|
const Style: FCWithChildren = ({children}) => {
|
||||||
return <ThemeProvider theme={makeTheme('dark')}>
|
return <ThemeProvider theme={makeTheme('dark')}>
|
||||||
<Container maxWidth='xl'>
|
<Box sx={{ }}/>
|
||||||
<Box sx={{ position: 'fixed', height: '100%', width: '100%', zIndex: -500, backgroundColor: 'rgb(0, 30, 60)', top: 0, left: 0 }}/>
|
|
||||||
{children}
|
{children}
|
||||||
</Container>
|
|
||||||
</ThemeProvider>;
|
</ThemeProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Add } from '@mui/icons-material';
|
import { Add } from '@mui/icons-material';
|
||||||
import { Box, Button, Dialog, Divider } from '@mui/material';
|
import { Box, Button, Dialog, Divider, Typography } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DownloadSelector from './DownloadSelector/DownloadSelector';
|
import DownloadSelector from './DownloadSelector/DownloadSelector';
|
||||||
import EpisodeListing from './DownloadSelector/Listing/EpisodeListing';
|
import EpisodeListing from './DownloadSelector/Listing/EpisodeListing';
|
||||||
|
|
@ -10,14 +10,14 @@ const AddToQueue: React.FC = () => {
|
||||||
|
|
||||||
return <Box>
|
return <Box>
|
||||||
<EpisodeListing />
|
<EpisodeListing />
|
||||||
<Dialog open={isOpen} onClose={() => setOpen(false)} maxWidth='md'>
|
<Dialog open={isOpen} onClose={() => setOpen(false)} maxWidth='md' PaperProps={{ elevation:4 }}>
|
||||||
<Box sx={{ border: '2px solid white', p: 2 }}>
|
<Box>
|
||||||
<SearchBox />
|
<SearchBox />
|
||||||
<Divider variant='middle' className="divider-width" light sx={{ color: 'text.primary', fontSize: '1.2rem' }}>Options</Divider>
|
<Divider variant='middle'/>
|
||||||
<DownloadSelector onFinish={() => setOpen(false)} />
|
<DownloadSelector onFinish={() => setOpen(false)} />
|
||||||
</Box>
|
</Box>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Button variant='contained' onClick={() => setOpen(true)}>
|
<Button variant='contained' onClick={() => setOpen(true)} sx={{ maxHeight: '2.3rem' }}>
|
||||||
<Add />
|
<Add />
|
||||||
Add to Queue
|
Add to Queue
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Button, TextField } from '@mui/material';
|
import { Box, Button, Divider, InputBase, Link, MenuItem, Select, TextField, Tooltip, Typography } from '@mui/material';
|
||||||
import useStore from '../../../hooks/useStore';
|
import useStore from '../../../hooks/useStore';
|
||||||
import MultiSelect from '../../reusable/MultiSelect';
|
import MultiSelect from '../../reusable/MultiSelect';
|
||||||
import { messageChannelContext } from '../../../provider/MessageChannel';
|
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||||
|
|
||||||
type DownloadSelectorProps = {
|
type DownloadSelectorProps = {
|
||||||
onFinish?: () => unknown
|
onFinish?: () => unknown
|
||||||
|
|
@ -78,14 +79,36 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
return <Box sx={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
<Box sx={{ m: 2, gap: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', flexWrap: 'wrap' }}>
|
<Box sx={{display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: '5px',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
width: '50rem',
|
||||||
|
height: '21rem',
|
||||||
|
margin: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
//backgroundColor: '#ffffff30',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.7rem',
|
||||||
|
//backgroundColor: '#ff000030'
|
||||||
|
}}>
|
||||||
|
<Typography sx={{fontSize: '1.4rem'}}>
|
||||||
|
General Options
|
||||||
|
</Typography>
|
||||||
<TextField value={store.downloadOptions.id} required onChange={e => {
|
<TextField value={store.downloadOptions.id} required onChange={e => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'downloadOptions',
|
type: 'downloadOptions',
|
||||||
payload: { ...store.downloadOptions, id: e.target.value }
|
payload: { ...store.downloadOptions, id: e.target.value }
|
||||||
});
|
});
|
||||||
}} label='Item ID' />
|
}} label='Show ID'/>
|
||||||
<TextField type='number' value={store.downloadOptions.q} required onChange={e => {
|
<TextField type='number' value={store.downloadOptions.q} required onChange={e => {
|
||||||
const parsed = parseInt(e.target.value);
|
const parsed = parseInt(e.target.value);
|
||||||
if (isNaN(parsed) || parsed < 0 || parsed > 10)
|
if (isNaN(parsed) || parsed < 0 || parsed > 10)
|
||||||
|
|
@ -94,13 +117,77 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
||||||
type: 'downloadOptions',
|
type: 'downloadOptions',
|
||||||
payload: { ...store.downloadOptions, q: parsed }
|
payload: { ...store.downloadOptions, q: parsed }
|
||||||
});
|
});
|
||||||
}} label='Quality Level (0 for max)' />
|
}} label='Quality Level (0 for max)'/>
|
||||||
<TextField disabled={store.downloadOptions.all} value={store.downloadOptions.e} required onChange={e => {
|
<Box sx={{ display: 'flex', gap: '5px' }}>
|
||||||
dispatch({
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, noaudio: !store.downloadOptions.noaudio } })} variant={store.downloadOptions.noaudio ? 'contained' : 'outlined'}>Skip Audio</Button>
|
||||||
type: 'downloadOptions',
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, novids: !store.downloadOptions.novids } })} variant={store.downloadOptions.novids ? 'contained' : 'outlined'}>Skip Video</Button>
|
||||||
payload: { ...store.downloadOptions, e: e.target.value }
|
</Box>
|
||||||
});
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, dlVideoOnce: !store.downloadOptions.dlVideoOnce } })} variant={store.downloadOptions.dlVideoOnce ? 'contained' : 'outlined'}>Skip Unnecessary</Button>
|
||||||
}} label='Episode Select' />
|
<Tooltip title={
|
||||||
|
<Typography>
|
||||||
|
Currently only supported on Hidive
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
arrow
|
||||||
|
placement='top'>
|
||||||
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, simul: !store.downloadOptions.simul } })} variant={store.downloadOptions.simul ? 'contained' : 'outlined'}>Download Simulcast ver.</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.7rem',
|
||||||
|
//backgroundColor: '#00000020'
|
||||||
|
}}>
|
||||||
|
<Typography sx={{fontSize: '1.4rem'}}>
|
||||||
|
Episode Options
|
||||||
|
</Typography>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '1px'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
borderColor: '#595959',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: '1px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
//backgroundColor: '#ff4567',
|
||||||
|
width: '15rem',
|
||||||
|
height: '3.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
'&:hover' : {
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
<InputBase sx={{
|
||||||
|
ml: 2,
|
||||||
|
flex: 1,
|
||||||
|
}}
|
||||||
|
disabled={store.downloadOptions.all} value={store.downloadOptions.e} required onChange={e => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, e: e.target.value }
|
||||||
|
});
|
||||||
|
}} placeholder='Episode Select'/>
|
||||||
|
<Divider orientation='vertical'/>
|
||||||
|
<LoadingButton loading={loading} disableElevation disableFocusRipple disableRipple disableTouchRipple onClick={listEpisodes} variant='text' sx={{ textTransform: 'none'}}><Typography>List<br/>Episodes</Typography></LoadingButton>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, all: !store.downloadOptions.all } })} variant={store.downloadOptions.all ? 'contained' : 'outlined'}>Download All</Button>
|
||||||
|
<Button sx={{ textTransform: 'none'}} onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, but: !store.downloadOptions.but } })} variant={store.downloadOptions.but ? 'contained' : 'outlined'}>Download All but</Button>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.7rem',
|
||||||
|
//backgroundColor: '#00ff0020'
|
||||||
|
}}>
|
||||||
|
<Typography sx={{fontSize: '1.4rem'}}>
|
||||||
|
Language Options
|
||||||
|
</Typography>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
title='Dub Languages'
|
title='Dub Languages'
|
||||||
values={availableDubs}
|
values={availableDubs}
|
||||||
|
|
@ -113,6 +200,7 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
||||||
}}
|
}}
|
||||||
allOption
|
allOption
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
title='Sub Languages'
|
title='Sub Languages'
|
||||||
values={availableSubs}
|
values={availableSubs}
|
||||||
|
|
@ -124,22 +212,105 @@ const DownloadSelector: React.FC<DownloadSelectorProps> = ({ onFinish }) => {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<TextField value={store.downloadOptions.fileName} onChange={e => {
|
<Tooltip title={
|
||||||
|
<Typography>
|
||||||
|
Comming Soon™
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
arrow placement='top'>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
gap: '1rem'
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Box sx={{
|
||||||
|
borderColor: '#595959',
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: '1px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
//backgroundColor: '#ff4567',
|
||||||
|
width: '15rem',
|
||||||
|
height: '3.5rem',
|
||||||
|
display: 'flex',
|
||||||
|
'&:hover' : {
|
||||||
|
borderColor: '#ffffff',
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<Button sx={{ textTransform: 'none' }} variant='outlined' disabled={true}>Hardsub</Button>
|
||||||
|
<Divider orientation='vertical'/>
|
||||||
|
<Select sx={{
|
||||||
|
flex: 1
|
||||||
|
}}
|
||||||
|
title='Hardsub lang.'
|
||||||
|
placeholder='Hardsub lang.'
|
||||||
|
disabled={true}
|
||||||
|
value={store.downloadOptions.hslang}
|
||||||
|
onChange={(e) => {
|
||||||
|
dispatch({
|
||||||
|
type: 'downloadOptions',
|
||||||
|
payload: { ...store.downloadOptions, hslang: (e.target.value as string) === '' ? undefined : e.target.value as string }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem>Deutsch</MenuItem>
|
||||||
|
</Select>
|
||||||
|
</Box>
|
||||||
|
<Tooltip title={
|
||||||
|
<Typography>
|
||||||
|
Burns the selected subtitle <b>PERMANENTLY</b> onto the video<br/>You can choose only <b>1</b> subtitle per video
|
||||||
|
</Typography>
|
||||||
|
} arrow placement='top'>
|
||||||
|
<InfoOutlinedIcon sx={{
|
||||||
|
transition: '100ms',
|
||||||
|
ml: '0.35rem',
|
||||||
|
mr: '0.65rem',
|
||||||
|
'&:hover' : {
|
||||||
|
color: '#ffffff30',
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginBottom: '20px'}}/>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
gap: '15px'
|
||||||
|
}}>
|
||||||
|
<TextField value={store.downloadOptions.fileName} onChange={e => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'downloadOptions',
|
type: 'downloadOptions',
|
||||||
payload: { ...store.downloadOptions, fileName: e.target.value }
|
payload: { ...store.downloadOptions, fileName: e.target.value }
|
||||||
});
|
});
|
||||||
}} sx={{ width: '50%' }} label='Filename' />
|
}} sx={{ width: '87%' }} label='Filename Overwrite' />
|
||||||
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, all: !store.downloadOptions.all } })} variant={store.downloadOptions.all ? 'contained' : 'outlined'}>Download all</Button>
|
<Tooltip title={
|
||||||
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, but: !store.downloadOptions.but } })} variant={store.downloadOptions.but ? 'contained' : 'outlined'}>Download all but</Button>
|
<Typography>
|
||||||
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, noaudio: !store.downloadOptions.noaudio } })} variant={store.downloadOptions.noaudio ? 'contained' : 'outlined'}>Skip Audio</Button>
|
Click here to see the documentation
|
||||||
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, novids: !store.downloadOptions.novids } })} variant={store.downloadOptions.novids ? 'contained' : 'outlined'}>Skip Video</Button>
|
</Typography>
|
||||||
<Button onClick={() => dispatch({ type: 'downloadOptions', payload: { ...store.downloadOptions, dlVideoOnce: !store.downloadOptions.dlVideoOnce } })} variant={store.downloadOptions.dlVideoOnce ? 'contained' : 'outlined'}>Skip unnecessary Downloads</Button>
|
} arrow placement='top'>
|
||||||
</Box>
|
<Link href='https://github.com/anidl/multi-downloader-nx/blob/master/docs/DOCUMENTATION.md#filename-template' rel="noopener noreferrer" target="_blank">
|
||||||
<Box sx={{ gap: 2, flex: 0, m: 1, mb: 3, display: 'flex', justifyContent: 'center' }}>
|
<InfoOutlinedIcon sx={{
|
||||||
<LoadingButton loading={loading} onClick={listEpisodes} variant='contained'>List episodes</LoadingButton>
|
transition: '100ms',
|
||||||
<LoadingButton loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
|
'&:hover' : {
|
||||||
|
color: '#ffffff30',
|
||||||
|
}
|
||||||
|
}} />
|
||||||
|
</Link>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box sx={{width: '95%', height: '0.3rem', backgroundColor: '#ffffff50', borderRadius: '10px', marginTop: '10px'}}/>
|
||||||
|
|
||||||
|
<LoadingButton sx={{ margin: '15px', textTransform: 'none' }} loading={loading} onClick={addToQueue} variant='contained'>Add to Queue</LoadingButton>
|
||||||
|
|
||||||
</Box>;
|
</Box>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const LogoutButton: React.FC = () => {
|
||||||
startIcon={<ExitToApp />}
|
startIcon={<ExitToApp />}
|
||||||
variant='contained'
|
variant='contained'
|
||||||
onClick={logout}
|
onClick={logout}
|
||||||
|
sx={{ maxHeight: '2.3rem' }}
|
||||||
>
|
>
|
||||||
Service select
|
Service select
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
.divider-width::before, .divider-width::after {
|
|
||||||
border-top: 3px solid white !important;
|
|
||||||
transform: translateY(40%) !important;
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import './MainFrame.css';
|
|
||||||
import Queue from './Queue/Queue';
|
import Queue from './Queue/Queue';
|
||||||
|
|
||||||
const MainFrame: React.FC = () => {
|
const MainFrame: React.FC = () => {
|
||||||
return <Box sx={{ marginLeft: 1 }}>
|
return <Box sx={{ }}>
|
||||||
<Queue />
|
<Queue />
|
||||||
</Box>;
|
</Box>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { Box, Button, CircularProgress, Divider, LinearProgress, Skeleton, Typography } from '@mui/material';
|
import { Badge, Box, Button, CircularProgress, Divider, IconButton, LinearProgress, Skeleton, Tooltip, Typography } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { messageChannelContext } from '../../../provider/MessageChannel';
|
import { messageChannelContext } from '../../../provider/MessageChannel';
|
||||||
import { queueContext } from '../../../provider/QueueProvider';
|
import { queueContext } from '../../../provider/QueueProvider';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
|
||||||
import useDownloadManager from '../DownloadManager/DownloadManager';
|
import useDownloadManager from '../DownloadManager/DownloadManager';
|
||||||
|
|
||||||
|
|
@ -16,102 +17,393 @@ const Queue: React.FC = () => {
|
||||||
|
|
||||||
return data || queue.length > 0 ? <>
|
return data || queue.length > 0 ? <>
|
||||||
{data && <>
|
{data && <>
|
||||||
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1, mb: 1, mt: 1 }}>
|
<Box sx={{
|
||||||
<img src={data.downloadInfo.image} height='auto' width='100%' alt="Thumbnail" />
|
display: 'flex',
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
width: '100%',
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
flexDirection: 'column',
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr max-content' }}>
|
alignItems: 'center',
|
||||||
<Typography variant='h5' color='text.primary'>
|
}}>
|
||||||
{data.downloadInfo.title}
|
<Box sx={{
|
||||||
</Typography>
|
marginTop: '2rem',
|
||||||
<Typography variant='h5' color='text.primary'>
|
marginBottom: '1rem',
|
||||||
Language: {data.downloadInfo.language.name}
|
height: '12rem',
|
||||||
</Typography>
|
width: '93vw',
|
||||||
</Box>
|
maxWidth: '93rem',
|
||||||
<Typography variant='h6' color='text.primary'>
|
backgroundColor: '#282828',
|
||||||
|
boxShadow: '0px 0px 50px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
transition: '250ms'
|
||||||
|
}}>
|
||||||
|
<img style={{
|
||||||
|
borderRadius: '5px',
|
||||||
|
margin: '5px',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
userSelect: 'none',
|
||||||
|
}}
|
||||||
|
src={data.downloadInfo.image} height='auto' width='auto' alt="Thumbnail" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
//backgroundColor: '#ff0000',
|
||||||
|
width: '70%',
|
||||||
|
marginLeft: '10px'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
flexDirection: 'column',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
}}>
|
||||||
{data.downloadInfo.parent.title}
|
{data.downloadInfo.parent.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
}}>
|
||||||
|
{data.downloadInfo.title}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<LinearProgress variant='determinate' sx={{ height: '10px' }} value={(typeof data.progress.percent === 'string' ? parseInt(data.progress.percent) : data.progress.percent)} />
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
//backgroundColor: '#00ff00',
|
||||||
|
width: '30%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
}}>
|
||||||
|
Downloading: {data.downloadInfo.language.name}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
height: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
//backgroundColor: '#0000ff',
|
||||||
|
}}>
|
||||||
|
<LinearProgress variant='determinate'
|
||||||
|
sx={{
|
||||||
|
height: '20px',
|
||||||
|
width: '97.53%',
|
||||||
|
margin: '10px',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}} value={(typeof data.progress.percent === 'string' ? parseInt(data.progress.percent) : data.progress.percent)}
|
||||||
|
/>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body1" color='text.primary'>
|
<Typography color='text.primary'
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.3rem',
|
||||||
|
}}>
|
||||||
{data.progress.cur} / {(data.progress.total)} parts ({data.progress.percent}% | {formatTime(data.progress.time)} | {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB)
|
{data.progress.cur} / {(data.progress.total)} parts ({data.progress.percent}% | {formatTime(data.progress.time)} | {(data.progress.downloadSpeed / 1024 / 1024).toFixed(2)} MB/s | {(data.progress.bytes / 1024 / 1024).toFixed(2)}MB)
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
current && !data && <>
|
current && !data && <>
|
||||||
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1, mb: 1, mt: 1 }}>
|
<Box sx={{
|
||||||
<img src={current.image} height='auto' width='100%' alt="Thumbnail" />
|
display: 'flex',
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
flexDirection: 'column',
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
alignItems: 'center',
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr max-content' }}>
|
}}>
|
||||||
<Typography variant='h5' color='text.primary'>
|
<Box sx={{
|
||||||
{current.title}
|
marginTop: '2rem',
|
||||||
</Typography>
|
marginBottom: '1rem',
|
||||||
<Typography variant='h5' color='text.primary'>
|
height: '12rem',
|
||||||
Language: <CircularProgress variant="indeterminate" />
|
width: '93vw',
|
||||||
</Typography>
|
maxWidth: '93rem',
|
||||||
</Box>
|
backgroundColor: '#282828',
|
||||||
<Typography variant='h6' color='text.primary'>
|
boxShadow: '0px 0px 50px #00000090',
|
||||||
{current.parent.title}
|
borderRadius: '10px',
|
||||||
</Typography>
|
display: 'flex',
|
||||||
</Box>
|
overflow: 'hidden',
|
||||||
<LinearProgress variant='indeterminate' sx={{ height: '10px' }} />
|
transition: '250ms'
|
||||||
<Box>
|
}}>
|
||||||
<Typography variant="body1" color='text.primary'>
|
<img style={{
|
||||||
0 / ? parts (0% | X:XX | 0 MB/s | 0MB)
|
borderRadius: '5px',
|
||||||
</Typography>
|
margin: '5px',
|
||||||
</Box>
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
userSelect: 'none',
|
||||||
|
maxWidth: '20.5rem',
|
||||||
|
}}
|
||||||
|
src={current.image} height='auto' width='auto' alt="Thumbnail" />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'center',
|
||||||
|
//backgroundColor: '#ffffff0f'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
width: '70%',
|
||||||
|
marginLeft: '10px'
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
flexDirection: 'column',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
}}>
|
||||||
|
{current.parent.title}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
}}>
|
||||||
|
{current.title}
|
||||||
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
//backgroundColor: '#00ff00',
|
||||||
|
width: '30%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
position: 'relative',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
}}>
|
||||||
|
Downloading:
|
||||||
|
</Typography>
|
||||||
|
<CircularProgress variant="indeterminate" sx={{
|
||||||
|
marginLeft: '2rem',
|
||||||
|
}}/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
height: '50%',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
//backgroundColor: '#0000ff',
|
||||||
|
}}>
|
||||||
|
<LinearProgress variant='indeterminate'
|
||||||
|
sx={{
|
||||||
|
height: '20px',
|
||||||
|
width: '97.53%',
|
||||||
|
margin: '10px',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography color='text.primary'
|
||||||
|
sx={{
|
||||||
|
fontSize: '1.3rem',
|
||||||
|
}}>
|
||||||
|
0 / ? parts (0% | XX:XX | 0 MB/s | 0MB)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
{queue.length && data && <Divider variant="fullWidth" />}
|
|
||||||
{queue.map((queueItem, index, { length }) => {
|
{queue.map((queueItem, index, { length }) => {
|
||||||
return <Box key={`queue_item_${index}`}>
|
return <Box key={`queue_item_${index}`} sx={{
|
||||||
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1, mb: 1, mt: 1 }}>
|
display: 'flex',
|
||||||
<img src={queueItem.image} height='auto' width='100%' alt="Thumbnail" />
|
mb: '-1.5rem',
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
flexDirection: 'column',
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
alignItems: 'center',
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '1fr 200px' }}>
|
}}>
|
||||||
<Typography variant='h5' color='text.primary'>
|
<Box sx={{
|
||||||
{queueItem.title}
|
marginTop: '1.5rem',
|
||||||
|
marginBottom: '1.5rem',
|
||||||
|
height: '11rem',
|
||||||
|
width: '90vw',
|
||||||
|
maxWidth: '90rem',
|
||||||
|
backgroundColor: '#282828',
|
||||||
|
boxShadow: '0px 0px 10px #00000090',
|
||||||
|
borderRadius: '10px',
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
<img style={{
|
||||||
|
borderRadius: '5px',
|
||||||
|
margin: '5px',
|
||||||
|
boxShadow: '0px 0px 5px #00000090',
|
||||||
|
userSelect: 'none',
|
||||||
|
maxWidth: '18.5rem'
|
||||||
|
}}
|
||||||
|
src={queueItem.image} height='auto' width='auto' alt="Thumbnail" />
|
||||||
|
<Box sx={{
|
||||||
|
margin: '5px',
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<Box sx={{
|
||||||
|
width: '30%',
|
||||||
|
marginRight: '5px',
|
||||||
|
marginLeft: '5px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
{queueItem.parent.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='h5' color='text.primary'>
|
<Typography color='text.primary' sx={{
|
||||||
Languages: {queueItem.dubLang.join(', ')}
|
fontSize: '1.6rem',
|
||||||
|
marginTop: '-0.4rem',
|
||||||
|
marginBottom: '0.4rem',
|
||||||
|
}}>
|
||||||
|
S{queueItem.parent.season}E{queueItem.episode}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.2rem',
|
||||||
|
marginTop: '-0.4rem',
|
||||||
|
marginBottom: '0.4rem',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
{queueItem.title}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
width: '40%',
|
||||||
|
marginRight: '5px',
|
||||||
|
marginLeft: '5px',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
Dub(s): {queueItem.dubLang.join(', ')}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}>
|
||||||
|
Sub(s): {queueItem.dlsubs.join(', ')}
|
||||||
|
</Typography>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '1.8rem',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
Quality: {queueItem.q}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant='h6' color='text.primary'>
|
<Box sx={{
|
||||||
{queueItem.parent.title}
|
marginRight: '5px',
|
||||||
</Typography>
|
marginLeft: '5px',
|
||||||
</Box>
|
width: '30%',
|
||||||
<Typography variant='body1' color='text.primary'>
|
justifyContent: 'center',
|
||||||
S{queueItem.parent.season}E{queueItem.episode} <br />
|
alignItems: 'center',
|
||||||
Quality: {queueItem.q}
|
display: 'flex'
|
||||||
</Typography>
|
}}>
|
||||||
<Button onClick={() => {
|
<Tooltip title="Delete from queue" arrow placement='top'>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
msg.removeFromQueue(index);
|
msg.removeFromQueue(index);
|
||||||
}} sx={{ position: 'relative', left: '50%', transform: 'translateX(-50%)', width: '60%' }} variant="outlined" color="warning">
|
}}
|
||||||
Remove from Queue
|
sx={{
|
||||||
</Button>
|
backgroundColor: '#ff573a25',
|
||||||
|
height: '40px',
|
||||||
|
transition: '250ms',
|
||||||
|
'&:hover' : {
|
||||||
|
backgroundColor: '#ff573a',
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{index < length - 1 && <Divider variant="fullWidth" />}
|
;
|
||||||
</Box>;
|
|
||||||
})}
|
})}
|
||||||
</> : <Box>
|
</> : <Box sx={{
|
||||||
<Typography color='text.primary' variant='h4'>
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
height: '12rem',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Typography color='text.primary' sx={{
|
||||||
|
fontSize: '2rem',
|
||||||
|
margin: '10px'
|
||||||
|
}}>
|
||||||
Selected episodes will be shown here
|
Selected episodes will be shown here
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box sx={{ height: 200, display: 'grid', gridTemplateColumns: '20% 1fr', gap: 1 }}>
|
<Box sx={{
|
||||||
<Skeleton variant='rectangular' height={'100%'}/>
|
display: 'flex',
|
||||||
<Box sx={{ display: 'grid', gridTemplateRows: '33% 1fr', gap: 1 }}>
|
margin: '10px'
|
||||||
<Skeleton variant='text' height={'100%'} />
|
}}>
|
||||||
<Skeleton variant='text' height={'100%'} />
|
<Skeleton variant='rectangular' height={'10rem'} width={'20rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
margin: '10px'
|
||||||
|
}}>
|
||||||
|
<Skeleton variant='rectangular' height={'10rem'} width={'20rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
<Box sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
|
<Skeleton variant='text' height={'100%'} width={'30rem'} sx={{ margin: '5px', borderRadius: '5px' }}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|
|
||||||
|
|
@ -44,13 +44,15 @@ const MenuBar: React.FC = () => {
|
||||||
if (!msg)
|
if (!msg)
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|
||||||
return <Box sx={{ width: '100%', display: 'flex' }}>
|
return <Box sx={{ display: 'flex', marginBottom: '1rem', width: '100%', alignItems: 'center' }}>
|
||||||
|
<Box sx={{ position: 'relative', left: '0%', width: '50%'}}>
|
||||||
<Button onClick={(e) => handleClick(e, 'settings')}>
|
<Button onClick={(e) => handleClick(e, 'settings')}>
|
||||||
Settings
|
Settings
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={(e) => handleClick(e, 'help')}>
|
<Button onClick={(e) => handleClick(e, 'help')}>
|
||||||
Help
|
Help
|
||||||
</Button>
|
</Button>
|
||||||
|
</Box>
|
||||||
<Menu open={openMenu === 'settings'} anchorEl={anchorEl} onClose={handleClose}>
|
<Menu open={openMenu === 'settings'} anchorEl={anchorEl} onClose={handleClose}>
|
||||||
<MenuItem onClick={() => {
|
<MenuItem onClick={() => {
|
||||||
msg.openFolder('config');
|
msg.openFolder('config');
|
||||||
|
|
@ -108,7 +110,7 @@ const MenuBar: React.FC = () => {
|
||||||
Version: {store.version}
|
Version: {store.version}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Typography variant="h5" color="text.primary" component="div" align="center" sx={{flexGrow: 1}}>
|
<Typography variant="h5" color="text.primary">
|
||||||
{transformService(store.service)}
|
{transformService(store.service)}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>;
|
</Box>;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ const StartQueueButton: React.FC = () => {
|
||||||
startIcon={start ? <PauseCircleFilled /> : <PlayCircleFilled /> }
|
startIcon={start ? <PauseCircleFilled /> : <PlayCircleFilled /> }
|
||||||
variant='contained'
|
variant='contained'
|
||||||
onClick={change}
|
onClick={change}
|
||||||
|
sx={{ maxHeight: '2.3rem' }}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
start ? 'Stop Queue' : 'Start Queue'
|
start ? 'Stop Queue' : 'Start Queue'
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,17 @@ import App from './App';
|
||||||
import ServiceProvider from './provider/ServiceProvider';
|
import ServiceProvider from './provider/ServiceProvider';
|
||||||
import Style from './Style';
|
import Style from './Style';
|
||||||
import MessageChannel from './provider/MessageChannel';
|
import MessageChannel from './provider/MessageChannel';
|
||||||
import { IconButton } from '@mui/material';
|
import { Box, IconButton } from '@mui/material';
|
||||||
import { CloseOutlined } from '@mui/icons-material';
|
import { CloseOutlined } from '@mui/icons-material';
|
||||||
import { SnackbarProvider, SnackbarKey } from 'notistack';
|
import { SnackbarProvider, SnackbarKey } from 'notistack';
|
||||||
import Store from './provider/Store';
|
import Store from './provider/Store';
|
||||||
import ErrorHandler from './provider/ErrorHandler';
|
import ErrorHandler from './provider/ErrorHandler';
|
||||||
import QueueProvider from './provider/QueueProvider';
|
import QueueProvider from './provider/QueueProvider';
|
||||||
|
|
||||||
|
document.body.style.backgroundColor = "rgb(0, 30, 60)";
|
||||||
|
document.body.style.display = 'flex';
|
||||||
|
document.body.style.justifyContent = 'center';
|
||||||
|
|
||||||
const notistackRef = React.createRef<SnackbarProvider>();
|
const notistackRef = React.createRef<SnackbarProvider>();
|
||||||
const onClickDismiss = (key: SnackbarKey | undefined) => () => {
|
const onClickDismiss = (key: SnackbarKey | undefined) => () => {
|
||||||
if (notistackRef.current)
|
if (notistackRef.current)
|
||||||
|
|
@ -34,7 +38,9 @@ root.render(
|
||||||
<MessageChannel>
|
<MessageChannel>
|
||||||
<ServiceProvider>
|
<ServiceProvider>
|
||||||
<QueueProvider>
|
<QueueProvider>
|
||||||
<App />
|
<Box>
|
||||||
|
<App />
|
||||||
|
</Box>
|
||||||
</QueueProvider>
|
</QueueProvider>
|
||||||
</ServiceProvider>
|
</ServiceProvider>
|
||||||
</MessageChannel>
|
</MessageChannel>
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,12 @@ const ServiceProvider: FCWithChildren = ({ children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return service === undefined ?
|
return service === undefined ?
|
||||||
<Box>
|
<Box sx={{ justifyContent: 'center', alignItems: 'center', display: 'flex', flexDirection: 'column', position: 'relative', top: '40vh'}}>
|
||||||
<Typography color="text.primary" variant='h3' sx={{ textAlign: 'center', mb: 5 }}>Please choose your service</Typography>
|
<Typography color="text.primary" variant='h3' sx={{ textAlign: 'center', mb: 5 }}>Please select your service</Typography>
|
||||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'center' }}>
|
||||||
<Button size='large' variant="contained" onClick={() => setService('funi')} startIcon={<Avatar src={'https://static.funimation.com/static/img/favicon.ico'} />}>Funimation</Button>
|
<Button size='large' variant="contained" onClick={() => setService('funi')} startIcon={<Avatar src={'https://static.funimation.com/static/img/favicon.ico'} />}>Funimation</Button>
|
||||||
<Divider orientation='vertical' flexItem />
|
|
||||||
<Button size='large' variant="contained" onClick={() => setService('crunchy')} startIcon={<Avatar src={'https://static.crunchyroll.com/cxweb/assets/img/favicons/favicon-32x32.png'} />}>Crunchyroll</Button>
|
<Button size='large' variant="contained" onClick={() => setService('crunchy')} startIcon={<Avatar src={'https://static.crunchyroll.com/cxweb/assets/img/favicons/favicon-32x32.png'} />}>Crunchyroll</Button>
|
||||||
<Divider orientation='vertical' flexItem />
|
<Button size='large' variant="contained" onClick={() => setService('hidive')} startIcon={<Avatar src={'https://static.diceplatform.com/prod/original/dce.hidive/settings/HIDIVE_AppLogo_1024x1024.0G0vK.jpg'} />}>Hidive</Button>
|
||||||
<Button size='large' variant="contained" onClick={() => setService('hidive')} startIcon={<Avatar src={'https://www.hidive.com/favicon.ico'} />}>Hidive</Button>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
: <serviceContext.Provider value={service}>
|
: <serviceContext.Provider value={service}>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ export type DownloadOptions = {
|
||||||
all: boolean,
|
all: boolean,
|
||||||
but: boolean,
|
but: boolean,
|
||||||
novids: boolean,
|
novids: boolean,
|
||||||
|
hslang?: string,
|
||||||
|
simul: boolean,
|
||||||
noaudio: boolean
|
noaudio: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +50,8 @@ const initialState: StoreState = {
|
||||||
all: false,
|
all: false,
|
||||||
but: false,
|
but: false,
|
||||||
noaudio: false,
|
noaudio: false,
|
||||||
novids: false
|
novids: false,
|
||||||
|
simul: false
|
||||||
},
|
},
|
||||||
service: undefined,
|
service: undefined,
|
||||||
episodeListing: [],
|
episodeListing: [],
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ class CrunchyHandler extends Base implements MessageHandler {
|
||||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||||
this.setDownloading(true);
|
this.setDownloading(true);
|
||||||
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
const _default = yargs.appArgv(this.crunchy.cfg.cli, true);
|
||||||
|
this.crunchy.api = _default.crapi;
|
||||||
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
const res = await this.crunchy.downloadFromSeriesID(data.id, {
|
||||||
dubLang: data.dubLang,
|
dubLang: data.dubLang,
|
||||||
e: data.e
|
e: data.e
|
||||||
|
|
@ -98,7 +99,7 @@ class CrunchyHandler extends Base implements MessageHandler {
|
||||||
if (res.isOk) {
|
if (res.isOk) {
|
||||||
for (const select of res.value) {
|
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, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
if (!(await this.crunchy.downloadEpisode(select, {..._default, skipsubs: false, callbackMaker: this.makeProgressHandler.bind(this), q: data.q, fileName: data.fileName, dlsubs: data.dlsubs, dlVideoOnce: data.dlVideoOnce, force: 'y',
|
||||||
novids: data.novids }))) {
|
novids: data.novids, hslang: data.hslang || 'none' }))) {
|
||||||
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
const er = new Error(`Unable to download episode ${data.e} from ${data.id}`);
|
||||||
er.name = 'Download error';
|
er.name = 'Download error';
|
||||||
this.alertError(er);
|
this.alertError(er);
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ class FunimationHandler extends Base implements MessageHandler {
|
||||||
},
|
},
|
||||||
image: a.image,
|
image: a.image,
|
||||||
e: a.episodeID,
|
e: a.episodeID,
|
||||||
episode: a.epsiodeNumber,
|
episode: a.epsiodeNumber
|
||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,13 @@ class HidiveHandler extends Base implements MessageHandler {
|
||||||
return { isOk: true, value: undefined };
|
return { isOk: true, value: undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getAPIVersion() {
|
||||||
|
const _default = yargs.appArgv(this.hidive.cfg.cli, true);
|
||||||
|
this.hidive.api = _default.hdapi;
|
||||||
|
}
|
||||||
|
|
||||||
public async search(data: SearchData): Promise<SearchResponse> {
|
public async search(data: SearchData): Promise<SearchResponse> {
|
||||||
|
await this.getAPIVersion();
|
||||||
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
console.debug(`Got search options: ${JSON.stringify(data)}`);
|
||||||
const hidiveSearch = await this.hidive.doSearch(data);
|
const hidiveSearch = await this.hidive.doSearch(data);
|
||||||
if (!hidiveSearch.isOk) {
|
if (!hidiveSearch.isOk) {
|
||||||
|
|
@ -42,7 +48,7 @@ class HidiveHandler extends Base implements MessageHandler {
|
||||||
public async availableDubCodes(): Promise<string[]> {
|
public async availableDubCodes(): Promise<string[]> {
|
||||||
const dubLanguageCodesArray: string[] = [];
|
const dubLanguageCodesArray: string[] = [];
|
||||||
for(const language of languages){
|
for(const language of languages){
|
||||||
if (language.hd_locale)
|
if (language.new_hd_locale)
|
||||||
dubLanguageCodesArray.push(language.code);
|
dubLanguageCodesArray.push(language.code);
|
||||||
}
|
}
|
||||||
return [...new Set(dubLanguageCodesArray)];
|
return [...new Set(dubLanguageCodesArray)];
|
||||||
|
|
@ -51,7 +57,7 @@ class HidiveHandler extends Base implements MessageHandler {
|
||||||
public async availableSubCodes(): Promise<string[]> {
|
public async availableSubCodes(): Promise<string[]> {
|
||||||
const subLanguageCodesArray: string[] = [];
|
const subLanguageCodesArray: string[] = [];
|
||||||
for(const language of languages){
|
for(const language of languages){
|
||||||
if (language.hd_locale)
|
if (language.new_hd_locale)
|
||||||
subLanguageCodesArray.push(language.locale);
|
subLanguageCodesArray.push(language.locale);
|
||||||
}
|
}
|
||||||
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
return ['all', 'none', ...new Set(subLanguageCodesArray)];
|
||||||
|
|
@ -62,63 +68,120 @@ class HidiveHandler extends Base implements MessageHandler {
|
||||||
if (isNaN(parse) || parse <= 0)
|
if (isNaN(parse) || parse <= 0)
|
||||||
return false;
|
return false;
|
||||||
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
console.debug(`Got resolve options: ${JSON.stringify(data)}`);
|
||||||
const res = await this.hidive.getShow(parseInt(data.id), data.e, data.but, data.all);
|
await this.getAPIVersion();
|
||||||
if (!res.isOk || !res.value)
|
if (this.hidive.api == 'old') {
|
||||||
return res.isOk;
|
const res = await this.hidive.getShow(parseInt(data.id), data.e, data.but, data.all);
|
||||||
this.addToQueue(res.value.map(item => {
|
if (!res.isOk || !res.value)
|
||||||
return {
|
return res.isOk;
|
||||||
...data,
|
this.addToQueue(res.value.map(item => {
|
||||||
ids: [item.Id],
|
return {
|
||||||
title: item.Name,
|
...data,
|
||||||
parent: {
|
ids: [item.Id],
|
||||||
title: item.seriesTitle,
|
title: item.Name,
|
||||||
season: parseFloat(item.SeasonNumberValue+'')+''
|
parent: {
|
||||||
},
|
title: item.seriesTitle,
|
||||||
image: item.ScreenShotSmallUrl,
|
season: parseFloat(item.SeasonNumberValue+'')+''
|
||||||
e: parseFloat(item.EpisodeNumberValue+'')+'',
|
},
|
||||||
episode: parseFloat(item.EpisodeNumberValue+'')+'',
|
image: item.ScreenShotSmallUrl,
|
||||||
};
|
e: parseFloat(item.EpisodeNumberValue+'')+'',
|
||||||
}));
|
episode: parseFloat(item.EpisodeNumberValue+'')+'',
|
||||||
return true;
|
};
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const res = await this.hidive.selectSeries(parseInt(data.id), data.e, data.but, data.all);
|
||||||
|
if (!res.isOk || !res.value)
|
||||||
|
return res.isOk;
|
||||||
|
this.addToQueue(res.value.map(item => {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
ids: [item.id],
|
||||||
|
title: item.title,
|
||||||
|
parent: {
|
||||||
|
title: item.seriesTitle,
|
||||||
|
season: item.episodeInformation.seasonNumber+''
|
||||||
|
},
|
||||||
|
image: item.thumbnailUrl,
|
||||||
|
e: item.episodeInformation.episodeNumber+'',
|
||||||
|
episode: item.episodeInformation.episodeNumber+'',
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
public async listEpisodes(id: string): Promise<EpisodeListResponse> {
|
||||||
const parse = parseInt(id);
|
const parse = parseInt(id);
|
||||||
if (isNaN(parse) || parse <= 0)
|
if (isNaN(parse) || parse <= 0)
|
||||||
return { isOk: false, reason: new Error('The ID is invalid') };
|
return { isOk: false, reason: new Error('The ID is invalid') };
|
||||||
const request = await this.hidive.listShow(parse);
|
|
||||||
if (!request.isOk || !request.value)
|
|
||||||
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
|
||||||
|
|
||||||
return { isOk: true, value: request.value.Episodes.map(function(item) {
|
await this.getAPIVersion();
|
||||||
const language = item.Summary.match(/^Audio: (.*)/m);
|
if (this.hidive.api == 'old') {
|
||||||
language?.shift();
|
const request = await this.hidive.listShow(parse);
|
||||||
const description = item.Summary.split('\r\n');
|
if (!request.isOk || !request.value)
|
||||||
return {
|
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||||
e: parseFloat(item.EpisodeNumberValue+'')+'',
|
|
||||||
lang: language ? language[0].split(', ') : [],
|
return { isOk: true, value: request.value.Episodes.map(function(item) {
|
||||||
name: item.Name,
|
const language = item.Summary.match(/^Audio: (.*)/m);
|
||||||
season: parseFloat(item.SeasonNumberValue+'')+'',
|
language?.shift();
|
||||||
seasonTitle: request.value.Name,
|
const description = item.Summary.split('\r\n');
|
||||||
episode: parseFloat(item.EpisodeNumberValue+'')+'',
|
return {
|
||||||
id: item.Id+'',
|
e: parseFloat(item.EpisodeNumberValue+'')+'',
|
||||||
img: item.ScreenShotSmallUrl,
|
lang: language ? language[0].split(', ') : [],
|
||||||
description: description ? description[0] : '',
|
name: item.Name,
|
||||||
time: ''
|
season: parseFloat(item.SeasonNumberValue+'')+'',
|
||||||
};
|
seasonTitle: request.value.Name,
|
||||||
})};
|
episode: parseFloat(item.EpisodeNumberValue+'')+'',
|
||||||
|
id: item.Id+'',
|
||||||
|
img: item.ScreenShotSmallUrl,
|
||||||
|
description: description ? description[0] : '',
|
||||||
|
time: ''
|
||||||
|
};
|
||||||
|
})};
|
||||||
|
} else {
|
||||||
|
const request = await this.hidive.listSeries(parse);
|
||||||
|
if (!request.isOk || !request.value)
|
||||||
|
return {isOk: false, reason: new Error('Unknown upstream error, check for additional logs')};
|
||||||
|
|
||||||
|
return { isOk: true, value: request.value.map(function(item) {
|
||||||
|
const description = item.description.split('\r\n');
|
||||||
|
return {
|
||||||
|
e: item.episodeInformation.episodeNumber+'',
|
||||||
|
lang: [],
|
||||||
|
name: item.title,
|
||||||
|
season: item.episodeInformation.seasonNumber+'',
|
||||||
|
seasonTitle: request.series.seasons[item.episodeInformation.seasonNumber-1].title,
|
||||||
|
episode: item.episodeInformation.episodeNumber+'',
|
||||||
|
id: item.id+'',
|
||||||
|
img: item.thumbnailUrl,
|
||||||
|
description: description ? description[0] : '',
|
||||||
|
time: ''
|
||||||
|
};
|
||||||
|
})};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async downloadItem(data: DownloadData) {
|
public async downloadItem(data: DownloadData) {
|
||||||
this.setDownloading(true);
|
this.setDownloading(true);
|
||||||
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
console.debug(`Got download options: ${JSON.stringify(data)}`);
|
||||||
const _default = yargs.appArgv(this.hidive.cfg.cli, true);
|
const _default = yargs.appArgv(this.hidive.cfg.cli, true);
|
||||||
const res = await this.hidive.getShow(parseInt(data.id), data.e, false, false);
|
this.hidive.api = _default.hdapi;
|
||||||
if (!res.isOk || !res.showData)
|
if (this.hidive.api == 'old') {
|
||||||
return this.alertError(new Error('Download failed upstream, check for additional logs'));
|
const res = await this.hidive.getShow(parseInt(data.id), data.e, false, false);
|
||||||
|
if (!res.isOk || !res.showData)
|
||||||
|
return this.alertError(new Error('Download failed upstream, check for additional logs'));
|
||||||
|
|
||||||
for (const ep of res.value) {
|
for (const ep of res.value) {
|
||||||
await this.hidive.getEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids});
|
await this.hidive.getEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await this.hidive.selectSeries(parseInt(data.id), data.e, false, false);
|
||||||
|
if (!res.isOk || !res.showData)
|
||||||
|
return this.alertError(new Error('Download failed upstream, check for additional logs'));
|
||||||
|
|
||||||
|
for (const ep of res.value) {
|
||||||
|
await this.hidive.downloadEpisode(ep, {..._default, callbackMaker: this.makeProgressHandler.bind(this), dubLang: data.dubLang, dlsubs: data.dlsubs, fileName: data.fileName, q: data.q, force: 'y', noaudio: data.noaudio, novids: data.novids });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.sendMessage({ name: 'finish', data: undefined });
|
this.sendMessage({ name: 'finish', data: undefined });
|
||||||
this.setDownloading(false);
|
this.setDownloading(false);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { console } from './log';
|
||||||
const buildsDir = './_builds';
|
const buildsDir = './_builds';
|
||||||
const nodeVer = 'node18-';
|
const nodeVer = 'node18-';
|
||||||
|
|
||||||
type BuildTypes = `${'ubuntu'|'windows'|'macos'|'arm'}64`
|
type BuildTypes = `${'windows'|'macos'|'linux'|'linuxstatic'|'alpine'}-${'x64'|'arm64'}`|'linuxstatic-armv7'
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const buildType = process.argv[2] as BuildTypes;
|
const buildType = process.argv[2] as BuildTypes;
|
||||||
|
|
@ -21,16 +21,23 @@ type BuildTypes = `${'ubuntu'|'windows'|'macos'|'arm'}64`
|
||||||
// main
|
// main
|
||||||
async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
||||||
const buildStr = 'multi-downloader-nx';
|
const buildStr = 'multi-downloader-nx';
|
||||||
const acceptableBuilds = ['windows64','ubuntu64','macos64'];
|
const acceptablePlatforms = ['windows','linux','linuxstatic','macos','alpine'];
|
||||||
|
const acceptableArchs = ['x64','arm64'];
|
||||||
|
const acceptableBuilds: string[] = ['linuxstatic-armv7'];
|
||||||
|
for (const platform of acceptablePlatforms) {
|
||||||
|
for (const arch of acceptableArchs) {
|
||||||
|
acceptableBuilds.push(platform+'-'+arch);
|
||||||
|
}
|
||||||
|
}
|
||||||
if(!acceptableBuilds.includes(buildType)){
|
if(!acceptableBuilds.includes(buildType)){
|
||||||
console.error('[ERROR] unknown build type!');
|
console.error('Unknown build type!');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
await modulesCleanup('.');
|
await modulesCleanup('.');
|
||||||
if(!fs.existsSync(buildsDir)){
|
if(!fs.existsSync(buildsDir)){
|
||||||
fs.mkdirSync(buildsDir);
|
fs.mkdirSync(buildsDir);
|
||||||
}
|
}
|
||||||
const buildFull = `${buildStr}-${buildType}-${gui ? 'gui' : 'cli'}`;
|
const buildFull = `${buildStr}-${getFriendlyName(buildType)}-${gui ? 'gui' : 'cli'}`;
|
||||||
const buildDir = `${buildsDir}/${buildFull}`;
|
const buildDir = `${buildsDir}/${buildFull}`;
|
||||||
if(fs.existsSync(buildDir)){
|
if(fs.existsSync(buildDir)){
|
||||||
fs.removeSync(buildDir);
|
fs.removeSync(buildDir);
|
||||||
|
|
@ -38,7 +45,7 @@ async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
||||||
fs.mkdirSync(buildDir);
|
fs.mkdirSync(buildDir);
|
||||||
const buildConfig = [
|
const buildConfig = [
|
||||||
gui ? 'gui.js' : 'index.js',
|
gui ? 'gui.js' : 'index.js',
|
||||||
'--target', nodeVer + getTarget(buildType),
|
'--target', nodeVer + buildType,
|
||||||
'--output', `${buildDir}/${pkg.short_name}`,
|
'--output', `${buildDir}/${pkg.short_name}`,
|
||||||
];
|
];
|
||||||
console.info(`[Build] Build configuration: ${buildFull}`);
|
console.info(`[Build] Build configuration: ${buildFull}`);
|
||||||
|
|
@ -51,6 +58,7 @@ async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
||||||
}
|
}
|
||||||
fs.mkdirSync(`${buildDir}/config`);
|
fs.mkdirSync(`${buildDir}/config`);
|
||||||
fs.mkdirSync(`${buildDir}/videos`);
|
fs.mkdirSync(`${buildDir}/videos`);
|
||||||
|
fs.mkdirSync(`${buildDir}/widevine`);
|
||||||
fs.copySync('./config/bin-path.yml', `${buildDir}/config/bin-path.yml`);
|
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/cli-defaults.yml', `${buildDir}/config/cli-defaults.yml`);
|
||||||
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
|
fs.copySync('./config/dir-path.yml', `${buildDir}/config/dir-path.yml`);
|
||||||
|
|
@ -70,15 +78,12 @@ async function buildBinary(buildType: BuildTypes, gui: boolean) {
|
||||||
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
|
execSync(`7z a -t7z "${buildsDir}/${buildFull}.7z" "${buildDir}"`,{stdio:[0,1,2]});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTarget(bt: string) : string {
|
function getFriendlyName(buildString: string): string {
|
||||||
switch(bt){
|
if (buildString.includes('armv7')) {
|
||||||
case 'windows64':
|
return 'android';
|
||||||
return 'windows-x64';
|
|
||||||
case 'ubuntu64':
|
|
||||||
return 'linux-x64';
|
|
||||||
case 'macos64':
|
|
||||||
return 'macos-x64';
|
|
||||||
default:
|
|
||||||
return 'windows-x64';
|
|
||||||
}
|
}
|
||||||
}
|
if (buildString.includes('linuxstatic')) {
|
||||||
|
buildString = buildString.replace('linuxstatic', 'linux');
|
||||||
|
}
|
||||||
|
return buildString;
|
||||||
|
}
|
||||||
114
modules/cmac.ts
Normal file
114
modules/cmac.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
//Originally from https://github.com/Frooastside/node-widevine/blob/main/src/cmac.ts
|
||||||
|
|
||||||
|
import crypto from 'crypto';
|
||||||
|
|
||||||
|
export class AES_CMAC {
|
||||||
|
private readonly BLOCK_SIZE = 16;
|
||||||
|
private readonly XOR_RIGHT = Buffer.from([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87]);
|
||||||
|
private readonly EMPTY_BLOCK_SIZE_BUFFER = Buffer.alloc(this.BLOCK_SIZE);
|
||||||
|
|
||||||
|
private _key: Buffer;
|
||||||
|
private _subkeys: { first: Buffer; second: Buffer };
|
||||||
|
|
||||||
|
public constructor(key: Buffer) {
|
||||||
|
if (![16, 24, 32].includes(key.length)) {
|
||||||
|
throw new Error('Key size must be 128, 192, or 256 bits.');
|
||||||
|
}
|
||||||
|
this._key = key;
|
||||||
|
this._subkeys = this._generateSubkeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
public calculate(message: Buffer): Buffer {
|
||||||
|
const blockCount = this._getBlockCount(message);
|
||||||
|
|
||||||
|
let x = this.EMPTY_BLOCK_SIZE_BUFFER;
|
||||||
|
let y;
|
||||||
|
|
||||||
|
for (let i = 0; i < blockCount - 1; i++) {
|
||||||
|
const from = i * this.BLOCK_SIZE;
|
||||||
|
const block = message.subarray(from, from + this.BLOCK_SIZE);
|
||||||
|
y = this._xor(x, block);
|
||||||
|
x = this._aes(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
y = this._xor(x, this._getLastBlock(message));
|
||||||
|
x = this._aes(y);
|
||||||
|
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateSubkeys(): { first: Buffer; second: Buffer } {
|
||||||
|
const l = this._aes(this.EMPTY_BLOCK_SIZE_BUFFER);
|
||||||
|
|
||||||
|
let first = this._bitShiftLeft(l);
|
||||||
|
if (l[0] & 0x80) {
|
||||||
|
first = this._xor(first, this.XOR_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
let second = this._bitShiftLeft(first);
|
||||||
|
if (first[0] & 0x80) {
|
||||||
|
second = this._xor(second, this.XOR_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { first: first, second: second };
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getBlockCount(message: Buffer): number {
|
||||||
|
const blockCount = Math.ceil(message.length / this.BLOCK_SIZE);
|
||||||
|
return blockCount === 0 ? 1 : blockCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _aes(message: Buffer): Buffer {
|
||||||
|
const cipher = crypto.createCipheriv(`aes-${this._key.length * 8}-cbc`, this._key, Buffer.alloc(this.BLOCK_SIZE));
|
||||||
|
const result = cipher.update(message).subarray(0, 16);
|
||||||
|
cipher.destroy();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getLastBlock(message: Buffer): Buffer {
|
||||||
|
const blockCount = this._getBlockCount(message);
|
||||||
|
const paddedBlock = this._padding(message, blockCount - 1);
|
||||||
|
|
||||||
|
let complete = false;
|
||||||
|
if (message.length > 0) {
|
||||||
|
complete = message.length % this.BLOCK_SIZE === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = complete ? this._subkeys.first : this._subkeys.second;
|
||||||
|
return this._xor(paddedBlock, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _padding(message: Buffer, blockIndex: number): Buffer {
|
||||||
|
const block = Buffer.alloc(this.BLOCK_SIZE);
|
||||||
|
|
||||||
|
const from = blockIndex * this.BLOCK_SIZE;
|
||||||
|
|
||||||
|
const slice = message.subarray(from, from + this.BLOCK_SIZE);
|
||||||
|
block.set(slice);
|
||||||
|
|
||||||
|
if (slice.length !== this.BLOCK_SIZE) {
|
||||||
|
block[slice.length] = 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _bitShiftLeft(input: Buffer): Buffer {
|
||||||
|
const output = Buffer.alloc(input.length);
|
||||||
|
let overflow = 0;
|
||||||
|
for (let i = input.length - 1; i >= 0; i--) {
|
||||||
|
output[i] = (input[i] << 1) | overflow;
|
||||||
|
overflow = input[i] & 0x80 ? 1 : 0;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _xor(a: Buffer, b: Buffer): Buffer {
|
||||||
|
const length = Math.min(a.length, b.length);
|
||||||
|
const output = Buffer.alloc(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
output[i] = a[i] ^ b[i];
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,7 +26,7 @@ const fixMiddleWare = (res: Response) => {
|
||||||
|
|
||||||
export type HLSCallback = (data: ProgressData) => unknown;
|
export type HLSCallback = (data: ProgressData) => unknown;
|
||||||
|
|
||||||
type M3U8Json = {
|
export type M3U8Json = {
|
||||||
segments: Record<string, unknown>[],
|
segments: Record<string, unknown>[],
|
||||||
mediaSequence?: number,
|
mediaSequence?: number,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
161
modules/license.ts
Normal file
161
modules/license.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
//Originaly from https://github.com/Frooastside/node-widevine/blob/main/src/license.ts
|
||||||
|
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import Long from 'long';
|
||||||
|
import { AES_CMAC } from './cmac';
|
||||||
|
import {
|
||||||
|
ClientIdentification,
|
||||||
|
License,
|
||||||
|
LicenseRequest,
|
||||||
|
LicenseRequest_RequestType,
|
||||||
|
LicenseType,
|
||||||
|
ProtocolVersion,
|
||||||
|
SignedMessage,
|
||||||
|
SignedMessage_MessageType,
|
||||||
|
SignedMessage_SessionKeyType,
|
||||||
|
WidevinePsshData
|
||||||
|
} from './license_protocol';
|
||||||
|
|
||||||
|
const WIDEVINE_SYSTEM_ID = new Uint8Array([237, 239, 139, 169, 121, 214, 74, 206, 163, 200, 39, 220, 213, 29, 33, 237]);
|
||||||
|
|
||||||
|
export type KeyContainer = {
|
||||||
|
kid: string;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContentDecryptionModule = {
|
||||||
|
privateKey: Buffer;
|
||||||
|
identifierBlob: Buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Session {
|
||||||
|
private _devicePrivateKey: crypto.KeyObject;
|
||||||
|
private _identifierBlob: ClientIdentification;
|
||||||
|
private _identifier: Buffer;
|
||||||
|
private _pssh: Buffer;
|
||||||
|
private _rawLicenseRequest?: Buffer;
|
||||||
|
|
||||||
|
constructor(contentDecryptionModule: ContentDecryptionModule, pssh: Buffer) {
|
||||||
|
this._devicePrivateKey = crypto.createPrivateKey(contentDecryptionModule.privateKey);
|
||||||
|
this._identifierBlob = ClientIdentification.decode(contentDecryptionModule.identifierBlob);
|
||||||
|
this._identifier = this._generateIdentifier();
|
||||||
|
this._pssh = pssh;
|
||||||
|
}
|
||||||
|
|
||||||
|
createLicenseRequest(): Buffer {
|
||||||
|
if (!this._pssh.subarray(12, 28).equals(Buffer.from(WIDEVINE_SYSTEM_ID))) {
|
||||||
|
throw new Error('the pssh is not an actuall pssh');
|
||||||
|
}
|
||||||
|
const pssh = this._parsePSSH(this._pssh);
|
||||||
|
if (!pssh) {
|
||||||
|
throw new Error('pssh is invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
const licenseRequest: LicenseRequest = {
|
||||||
|
type: LicenseRequest_RequestType.NEW,
|
||||||
|
clientId: this._identifierBlob,
|
||||||
|
contentId: {
|
||||||
|
widevinePsshData: {
|
||||||
|
psshData: [this._pssh.subarray(32)],
|
||||||
|
licenseType: LicenseType.STREAMING,
|
||||||
|
requestId: this._identifier
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestTime: Long.fromNumber(Date.now()).divide(1000),
|
||||||
|
protocolVersion: ProtocolVersion.VERSION_2_1,
|
||||||
|
keyControlNonce: crypto.randomInt(2 ** 31),
|
||||||
|
keyControlNonceDeprecated: Buffer.alloc(0),
|
||||||
|
encryptedClientId: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
this._rawLicenseRequest = Buffer.from(LicenseRequest.encode(licenseRequest).finish());
|
||||||
|
|
||||||
|
const signature = crypto
|
||||||
|
.createSign('sha1')
|
||||||
|
.update(this._rawLicenseRequest)
|
||||||
|
.sign({ key: this._devicePrivateKey, padding: crypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: 20 });
|
||||||
|
|
||||||
|
const signedLicenseRequest: SignedMessage = {
|
||||||
|
type: SignedMessage_MessageType.LICENSE_REQUEST,
|
||||||
|
msg: this._rawLicenseRequest,
|
||||||
|
signature: Buffer.from(signature),
|
||||||
|
sessionKey: Buffer.alloc(0),
|
||||||
|
remoteAttestation: Buffer.alloc(0),
|
||||||
|
metricData: [],
|
||||||
|
serviceVersionInfo: undefined,
|
||||||
|
sessionKeyType: SignedMessage_SessionKeyType.UNDEFINED,
|
||||||
|
oemcryptoCoreMessage: Buffer.alloc(0)
|
||||||
|
};
|
||||||
|
|
||||||
|
return Buffer.from(SignedMessage.encode(signedLicenseRequest).finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
parseLicense(rawLicense: Buffer) {
|
||||||
|
if (!this._rawLicenseRequest) {
|
||||||
|
throw new Error('please request a license first');
|
||||||
|
}
|
||||||
|
const signedLicense = SignedMessage.decode(rawLicense);
|
||||||
|
const sessionKey = crypto.privateDecrypt(this._devicePrivateKey, signedLicense.sessionKey);
|
||||||
|
|
||||||
|
const cmac = new AES_CMAC(Buffer.from(sessionKey));
|
||||||
|
|
||||||
|
const encKeyBase = Buffer.concat([
|
||||||
|
Buffer.from('ENCRYPTION'),
|
||||||
|
Buffer.from('\x00', 'ascii'),
|
||||||
|
this._rawLicenseRequest,
|
||||||
|
Buffer.from('\x00\x00\x00\x80', 'ascii')
|
||||||
|
]);
|
||||||
|
const authKeyBase = Buffer.concat([
|
||||||
|
Buffer.from('AUTHENTICATION'),
|
||||||
|
Buffer.from('\x00', 'ascii'),
|
||||||
|
this._rawLicenseRequest,
|
||||||
|
Buffer.from('\x00\x00\x02\x00', 'ascii')
|
||||||
|
]);
|
||||||
|
|
||||||
|
const encKey = cmac.calculate(Buffer.concat([Buffer.from('\x01'), encKeyBase]));
|
||||||
|
const serverKey = Buffer.concat([
|
||||||
|
cmac.calculate(Buffer.concat([Buffer.from('\x01'), authKeyBase])),
|
||||||
|
cmac.calculate(Buffer.concat([Buffer.from('\x02'), authKeyBase]))
|
||||||
|
]);
|
||||||
|
/*const clientKey = Buffer.concat([
|
||||||
|
cmac.calculate(Buffer.concat([Buffer.from("\x03"), authKeyBase])),
|
||||||
|
cmac.calculate(Buffer.concat([Buffer.from("\x04"), authKeyBase]))
|
||||||
|
]);*/
|
||||||
|
|
||||||
|
const calculatedSignature = crypto.createHmac('sha256', serverKey).update(signedLicense.msg).digest();
|
||||||
|
|
||||||
|
if (!calculatedSignature.equals(signedLicense.signature)) {
|
||||||
|
throw new Error('signatures do not match');
|
||||||
|
}
|
||||||
|
|
||||||
|
const license = License.decode(signedLicense.msg);
|
||||||
|
|
||||||
|
return license.key.map((keyContainer) => {
|
||||||
|
const keyId = keyContainer.id.length ? keyContainer.id.toString('hex') : keyContainer.type.toString();
|
||||||
|
const decipher = crypto.createDecipheriv(`aes-${encKey.length * 8}-cbc`, encKey, keyContainer.iv);
|
||||||
|
const decryptedKey = decipher.update(keyContainer.key);
|
||||||
|
decipher.destroy();
|
||||||
|
const key: KeyContainer = {
|
||||||
|
kid: keyId,
|
||||||
|
key: decryptedKey.toString('hex')
|
||||||
|
};
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parsePSSH(pssh: Buffer): WidevinePsshData | null {
|
||||||
|
try {
|
||||||
|
return WidevinePsshData.decode(pssh.subarray(32));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _generateIdentifier(): Buffer {
|
||||||
|
return Buffer.from(`${crypto.randomBytes(8).toString('hex')}${'01'}${'00000000000000'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
get pssh(): Buffer {
|
||||||
|
return this._pssh;
|
||||||
|
}
|
||||||
|
}
|
||||||
749
modules/license_protocol.proto
Normal file
749
modules/license_protocol.proto
Normal file
|
|
@ -0,0 +1,749 @@
|
||||||
|
//Originally from https://github.com/Frooastside/node-widevine/blob/main/src/license_protocol.proto
|
||||||
|
|
||||||
|
syntax = "proto2";
|
||||||
|
|
||||||
|
package license_protocol;
|
||||||
|
|
||||||
|
enum LicenseType {
|
||||||
|
STREAMING = 1;
|
||||||
|
OFFLINE = 2;
|
||||||
|
// License type decision is left to provider.
|
||||||
|
AUTOMATIC = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PlatformVerificationStatus {
|
||||||
|
// The platform is not verified.
|
||||||
|
PLATFORM_UNVERIFIED = 0;
|
||||||
|
// Tampering detected on the platform.
|
||||||
|
PLATFORM_TAMPERED = 1;
|
||||||
|
// The platform has been verified by means of software.
|
||||||
|
PLATFORM_SOFTWARE_VERIFIED = 2;
|
||||||
|
// The platform has been verified by means of hardware (e.g. secure boot).
|
||||||
|
PLATFORM_HARDWARE_VERIFIED = 3;
|
||||||
|
// Platform verification was not performed.
|
||||||
|
PLATFORM_NO_VERIFICATION = 4;
|
||||||
|
// Platform and secure storage capability have been verified by means of
|
||||||
|
// software.
|
||||||
|
PLATFORM_SECURE_STORAGE_SOFTWARE_VERIFIED = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LicenseIdentification is propagated from LicenseRequest to License,
|
||||||
|
// incrementing version with each iteration.
|
||||||
|
message LicenseIdentification {
|
||||||
|
optional bytes request_id = 1;
|
||||||
|
optional bytes session_id = 2;
|
||||||
|
optional bytes purchase_id = 3;
|
||||||
|
optional LicenseType type = 4;
|
||||||
|
optional int32 version = 5;
|
||||||
|
optional bytes provider_session_token = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message License {
|
||||||
|
message Policy {
|
||||||
|
// Indicates that playback of the content is allowed.
|
||||||
|
optional bool can_play = 1 [default = false];
|
||||||
|
|
||||||
|
// Indicates that the license may be persisted to non-volatile
|
||||||
|
// storage for offline use.
|
||||||
|
optional bool can_persist = 2 [default = false];
|
||||||
|
|
||||||
|
// Indicates that renewal of this license is allowed.
|
||||||
|
optional bool can_renew = 3 [default = false];
|
||||||
|
|
||||||
|
// For the |*duration*| fields, playback must halt when
|
||||||
|
// license_start_time (seconds since the epoch (UTC)) +
|
||||||
|
// license_duration_seconds is exceeded. A value of 0
|
||||||
|
// indicates that there is no limit to the duration.
|
||||||
|
|
||||||
|
// Indicates the rental window.
|
||||||
|
optional int64 rental_duration_seconds = 4 [default = 0];
|
||||||
|
|
||||||
|
// Indicates the viewing window, once playback has begun.
|
||||||
|
optional int64 playback_duration_seconds = 5 [default = 0];
|
||||||
|
|
||||||
|
// Indicates the time window for this specific license.
|
||||||
|
optional int64 license_duration_seconds = 6 [default = 0];
|
||||||
|
|
||||||
|
// The |renewal*| fields only apply if |can_renew| is true.
|
||||||
|
|
||||||
|
// The window of time, in which playback is allowed to continue while
|
||||||
|
// renewal is attempted, yet unsuccessful due to backend problems with
|
||||||
|
// the license server.
|
||||||
|
optional int64 renewal_recovery_duration_seconds = 7 [default = 0];
|
||||||
|
|
||||||
|
// All renewal requests for this license shall be directed to the
|
||||||
|
// specified URL.
|
||||||
|
optional string renewal_server_url = 8;
|
||||||
|
|
||||||
|
// How many seconds after license_start_time, before renewal is first
|
||||||
|
// attempted.
|
||||||
|
optional int64 renewal_delay_seconds = 9 [default = 0];
|
||||||
|
|
||||||
|
// Specifies the delay in seconds between subsequent license
|
||||||
|
// renewal requests, in case of failure.
|
||||||
|
optional int64 renewal_retry_interval_seconds = 10 [default = 0];
|
||||||
|
|
||||||
|
// Indicates that the license shall be sent for renewal when usage is
|
||||||
|
// started.
|
||||||
|
optional bool renew_with_usage = 11 [default = false];
|
||||||
|
|
||||||
|
// Indicates to client that license renewal and release requests ought to
|
||||||
|
// include ClientIdentification (client_id).
|
||||||
|
optional bool always_include_client_id = 12 [default = false];
|
||||||
|
|
||||||
|
// Duration of grace period before playback_duration_seconds (short window)
|
||||||
|
// goes into effect. Optional.
|
||||||
|
optional int64 play_start_grace_period_seconds = 13 [default = 0];
|
||||||
|
|
||||||
|
// Enables "soft enforcement" of playback_duration_seconds, letting the user
|
||||||
|
// finish playback even if short window expires. Optional.
|
||||||
|
optional bool soft_enforce_playback_duration = 14 [default = false];
|
||||||
|
|
||||||
|
// Enables "soft enforcement" of rental_duration_seconds. Initial playback
|
||||||
|
// must always start before rental duration expires. In order to allow
|
||||||
|
// subsequent playbacks to start after the rental duration expires,
|
||||||
|
// soft_enforce_playback_duration must be true. Otherwise, subsequent
|
||||||
|
// playbacks will not be allowed once rental duration expires. Optional.
|
||||||
|
optional bool soft_enforce_rental_duration = 15 [default = true];
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyContainer {
|
||||||
|
enum KeyType {
|
||||||
|
SIGNING = 1; // Exactly one key of this type must appear.
|
||||||
|
CONTENT = 2; // Content key.
|
||||||
|
KEY_CONTROL = 3; // Key control block for license renewals. No key.
|
||||||
|
OPERATOR_SESSION = 4; // wrapped keys for auxiliary crypto operations.
|
||||||
|
ENTITLEMENT = 5; // Entitlement keys.
|
||||||
|
OEM_CONTENT = 6; // Partner-specific content key.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The SecurityLevel enumeration allows the server to communicate the level
|
||||||
|
// of robustness required by the client, in order to use the key.
|
||||||
|
enum SecurityLevel {
|
||||||
|
// Software-based whitebox crypto is required.
|
||||||
|
SW_SECURE_CRYPTO = 1;
|
||||||
|
|
||||||
|
// Software crypto and an obfuscated decoder is required.
|
||||||
|
SW_SECURE_DECODE = 2;
|
||||||
|
|
||||||
|
// The key material and crypto operations must be performed within a
|
||||||
|
// hardware backed trusted execution environment.
|
||||||
|
HW_SECURE_CRYPTO = 3;
|
||||||
|
|
||||||
|
// The crypto and decoding of content must be performed within a hardware
|
||||||
|
// backed trusted execution environment.
|
||||||
|
HW_SECURE_DECODE = 4;
|
||||||
|
|
||||||
|
// The crypto, decoding and all handling of the media (compressed and
|
||||||
|
// uncompressed) must be handled within a hardware backed trusted
|
||||||
|
// execution environment.
|
||||||
|
HW_SECURE_ALL = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
message KeyControl {
|
||||||
|
// |key_control| is documented in:
|
||||||
|
// Widevine Modular DRM Security Integration Guide for CENC
|
||||||
|
// If present, the key control must be communicated to the secure
|
||||||
|
// environment prior to any usage. This message is automatically generated
|
||||||
|
// by the Widevine License Server SDK.
|
||||||
|
optional bytes key_control_block = 1;
|
||||||
|
optional bytes iv = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OutputProtection {
|
||||||
|
// Indicates whether HDCP is required on digital outputs, and which
|
||||||
|
// version should be used.
|
||||||
|
enum HDCP {
|
||||||
|
HDCP_NONE = 0;
|
||||||
|
HDCP_V1 = 1;
|
||||||
|
HDCP_V2 = 2;
|
||||||
|
HDCP_V2_1 = 3;
|
||||||
|
HDCP_V2_2 = 4;
|
||||||
|
HDCP_V2_3 = 5;
|
||||||
|
HDCP_NO_DIGITAL_OUTPUT = 0xff;
|
||||||
|
}
|
||||||
|
optional HDCP hdcp = 1 [default = HDCP_NONE];
|
||||||
|
|
||||||
|
// Indicate the CGMS setting to be inserted on analog output.
|
||||||
|
enum CGMS {
|
||||||
|
CGMS_NONE = 42;
|
||||||
|
COPY_FREE = 0;
|
||||||
|
COPY_ONCE = 2;
|
||||||
|
COPY_NEVER = 3;
|
||||||
|
}
|
||||||
|
optional CGMS cgms_flags = 2 [default = CGMS_NONE];
|
||||||
|
|
||||||
|
enum HdcpSrmRule {
|
||||||
|
HDCP_SRM_RULE_NONE = 0;
|
||||||
|
// In 'required_protection', this means most current SRM is required.
|
||||||
|
// Update the SRM on the device. If update cannot happen,
|
||||||
|
// do not allow the key.
|
||||||
|
// In 'requested_protection', this means most current SRM is requested.
|
||||||
|
// Update the SRM on the device. If update cannot happen,
|
||||||
|
// allow use of the key anyway.
|
||||||
|
CURRENT_SRM = 1;
|
||||||
|
}
|
||||||
|
optional HdcpSrmRule hdcp_srm_rule = 3 [default = HDCP_SRM_RULE_NONE];
|
||||||
|
// Optional requirement to indicate analog output is not allowed.
|
||||||
|
optional bool disable_analog_output = 4 [default = false];
|
||||||
|
// Optional requirement to indicate digital output is not allowed.
|
||||||
|
optional bool disable_digital_output = 5 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
message VideoResolutionConstraint {
|
||||||
|
// Minimum and maximum video resolutions in the range (height x width).
|
||||||
|
optional uint32 min_resolution_pixels = 1;
|
||||||
|
optional uint32 max_resolution_pixels = 2;
|
||||||
|
// Optional output protection requirements for this range. If not
|
||||||
|
// specified, the OutputProtection in the KeyContainer applies.
|
||||||
|
optional OutputProtection required_protection = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message OperatorSessionKeyPermissions {
|
||||||
|
// Permissions/key usage flags for operator service keys
|
||||||
|
// (type = OPERATOR_SESSION).
|
||||||
|
optional bool allow_encrypt = 1 [default = false];
|
||||||
|
optional bool allow_decrypt = 2 [default = false];
|
||||||
|
optional bool allow_sign = 3 [default = false];
|
||||||
|
optional bool allow_signature_verify = 4 [default = false];
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes id = 1;
|
||||||
|
optional bytes iv = 2;
|
||||||
|
optional bytes key = 3;
|
||||||
|
optional KeyType type = 4;
|
||||||
|
optional SecurityLevel level = 5 [default = SW_SECURE_CRYPTO];
|
||||||
|
optional OutputProtection required_protection = 6;
|
||||||
|
// NOTE: Use of requested_protection is not recommended as it is only
|
||||||
|
// supported on a small number of platforms.
|
||||||
|
optional OutputProtection requested_protection = 7;
|
||||||
|
optional KeyControl key_control = 8;
|
||||||
|
optional OperatorSessionKeyPermissions operator_session_key_permissions = 9;
|
||||||
|
// Optional video resolution constraints. If the video resolution of the
|
||||||
|
// content being decrypted/decoded falls within one of the specified ranges,
|
||||||
|
// the optional required_protections may be applied. Otherwise an error will
|
||||||
|
// be reported.
|
||||||
|
// NOTE: Use of this feature is not recommended, as it is only supported on
|
||||||
|
// a small number of platforms.
|
||||||
|
repeated VideoResolutionConstraint video_resolution_constraints = 10;
|
||||||
|
// Optional flag to indicate the key must only be used if the client
|
||||||
|
// supports anti rollback of the user table. Content provider can query the
|
||||||
|
// client capabilities to determine if the client support this feature.
|
||||||
|
optional bool anti_rollback_usage_table = 11 [default = false];
|
||||||
|
// Optional not limited to commonly known track types such as SD, HD.
|
||||||
|
// It can be some provider defined label to identify the track.
|
||||||
|
optional string track_label = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional LicenseIdentification id = 1;
|
||||||
|
optional Policy policy = 2;
|
||||||
|
repeated KeyContainer key = 3;
|
||||||
|
// Time of the request in seconds (UTC) as set in
|
||||||
|
// LicenseRequest.request_time. If this time is not set in the request,
|
||||||
|
// the local time at the license service is used in this field.
|
||||||
|
optional int64 license_start_time = 4;
|
||||||
|
optional bool remote_attestation_verified = 5 [default = false];
|
||||||
|
// Client token generated by the content provider. Optional.
|
||||||
|
optional bytes provider_client_token = 6;
|
||||||
|
// 4cc code specifying the CENC protection scheme as defined in the CENC 3.0
|
||||||
|
// specification. Propagated from Widevine PSSH box. Optional.
|
||||||
|
optional uint32 protection_scheme = 7;
|
||||||
|
// 8 byte verification field "HDCPDATA" followed by unsigned 32 bit minimum
|
||||||
|
// HDCP SRM version (whether the version is for HDCP1 SRM or HDCP2 SRM
|
||||||
|
// depends on client max_hdcp_version).
|
||||||
|
// Additional details can be found in Widevine Modular DRM Security
|
||||||
|
// Integration Guide for CENC.
|
||||||
|
optional bytes srm_requirement = 8;
|
||||||
|
// If present this contains a signed SRM file (either HDCP1 SRM or HDCP2 SRM
|
||||||
|
// depending on client max_hdcp_version) that should be installed on the
|
||||||
|
// client device.
|
||||||
|
optional bytes srm_update = 9;
|
||||||
|
// Indicates the status of any type of platform verification performed by the
|
||||||
|
// server.
|
||||||
|
optional PlatformVerificationStatus platform_verification_status = 10
|
||||||
|
[default = PLATFORM_NO_VERIFICATION];
|
||||||
|
// IDs of the groups for which keys are delivered in this license, if any.
|
||||||
|
repeated bytes group_ids = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProtocolVersion {
|
||||||
|
VERSION_2_0 = 20;
|
||||||
|
VERSION_2_1 = 21;
|
||||||
|
VERSION_2_2 = 22;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LicenseRequest {
|
||||||
|
message ContentIdentification {
|
||||||
|
message WidevinePsshData {
|
||||||
|
repeated bytes pssh_data = 1;
|
||||||
|
optional LicenseType license_type = 2;
|
||||||
|
optional bytes request_id = 3; // Opaque, client-specified.
|
||||||
|
}
|
||||||
|
|
||||||
|
message WebmKeyId {
|
||||||
|
optional bytes header = 1;
|
||||||
|
optional LicenseType license_type = 2;
|
||||||
|
optional bytes request_id = 3; // Opaque, client-specified.
|
||||||
|
}
|
||||||
|
|
||||||
|
message ExistingLicense {
|
||||||
|
optional LicenseIdentification license_id = 1;
|
||||||
|
optional int64 seconds_since_started = 2;
|
||||||
|
optional int64 seconds_since_last_played = 3;
|
||||||
|
optional bytes session_usage_table_entry = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message InitData {
|
||||||
|
enum InitDataType {
|
||||||
|
CENC = 1;
|
||||||
|
WEBM = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional InitDataType init_data_type = 1 [default = CENC];
|
||||||
|
optional bytes init_data = 2;
|
||||||
|
optional LicenseType license_type = 3;
|
||||||
|
optional bytes request_id = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
oneof content_id_variant {
|
||||||
|
// Exactly one of these must be present.
|
||||||
|
WidevinePsshData widevine_pssh_data = 1;
|
||||||
|
WebmKeyId webm_key_id = 2;
|
||||||
|
ExistingLicense existing_license = 3;
|
||||||
|
InitData init_data = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RequestType {
|
||||||
|
NEW = 1;
|
||||||
|
RENEWAL = 2;
|
||||||
|
RELEASE = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The client_id provides information authenticating the calling device. It
|
||||||
|
// contains the Widevine keybox token that was installed on the device at the
|
||||||
|
// factory. This field or encrypted_client_id below is required for a valid
|
||||||
|
// license request, but both should never be present in the same request.
|
||||||
|
optional ClientIdentification client_id = 1;
|
||||||
|
optional ContentIdentification content_id = 2;
|
||||||
|
optional RequestType type = 3;
|
||||||
|
// Time of the request in seconds (UTC) as set by the client.
|
||||||
|
optional int64 request_time = 4;
|
||||||
|
// Old-style decimal-encoded string key control nonce.
|
||||||
|
optional bytes key_control_nonce_deprecated = 5;
|
||||||
|
optional ProtocolVersion protocol_version = 6 [default = VERSION_2_0];
|
||||||
|
// New-style uint32 key control nonce, please use instead of
|
||||||
|
// key_control_nonce_deprecated.
|
||||||
|
optional uint32 key_control_nonce = 7;
|
||||||
|
// Encrypted ClientIdentification message, used for privacy purposes.
|
||||||
|
optional EncryptedClientIdentification encrypted_client_id = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
message MetricData {
|
||||||
|
enum MetricType {
|
||||||
|
// The time spent in the 'stage', specified in microseconds.
|
||||||
|
LATENCY = 1;
|
||||||
|
// The UNIX epoch timestamp at which the 'stage' was first accessed in
|
||||||
|
// microseconds.
|
||||||
|
TIMESTAMP = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TypeValue {
|
||||||
|
optional MetricType type = 1;
|
||||||
|
// The value associated with 'type'. For example if type == LATENCY, the
|
||||||
|
// value would be the time in microseconds spent in this 'stage'.
|
||||||
|
optional int64 value = 2 [default = 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'stage' that is currently processing the SignedMessage. Required.
|
||||||
|
optional string stage_name = 1;
|
||||||
|
// metric and associated value.
|
||||||
|
repeated TypeValue metric_data = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message VersionInfo {
|
||||||
|
// License SDK version reported by the Widevine License SDK. This field
|
||||||
|
// is populated automatically by the SDK.
|
||||||
|
optional string license_sdk_version = 1;
|
||||||
|
// Version of the service hosting the license SDK. This field is optional.
|
||||||
|
// It may be provided by the hosting service.
|
||||||
|
optional string license_service_version = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SignedMessage {
|
||||||
|
enum MessageType {
|
||||||
|
LICENSE_REQUEST = 1;
|
||||||
|
LICENSE = 2;
|
||||||
|
ERROR_RESPONSE = 3;
|
||||||
|
SERVICE_CERTIFICATE_REQUEST = 4;
|
||||||
|
SERVICE_CERTIFICATE = 5;
|
||||||
|
SUB_LICENSE = 6;
|
||||||
|
CAS_LICENSE_REQUEST = 7;
|
||||||
|
CAS_LICENSE = 8;
|
||||||
|
EXTERNAL_LICENSE_REQUEST = 9;
|
||||||
|
EXTERNAL_LICENSE = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SessionKeyType {
|
||||||
|
UNDEFINED = 0;
|
||||||
|
WRAPPED_AES_KEY = 1;
|
||||||
|
EPHERMERAL_ECC_PUBLIC_KEY = 2;
|
||||||
|
}
|
||||||
|
optional MessageType type = 1;
|
||||||
|
optional bytes msg = 2;
|
||||||
|
// Required field that contains the signature of the bytes of msg.
|
||||||
|
// For license requests, the signing algorithm is determined by the
|
||||||
|
// certificate contained in the request.
|
||||||
|
// For license responses, the signing algorithm is HMAC with signing key based
|
||||||
|
// on |session_key|.
|
||||||
|
optional bytes signature = 3;
|
||||||
|
// If populated, the contents of this field will be signaled by the
|
||||||
|
// |session_key_type| type. If the |session_key_type| is WRAPPED_AES_KEY the
|
||||||
|
// key is the bytes of an encrypted AES key. If the |session_key_type| is
|
||||||
|
// EPHERMERAL_ECC_PUBLIC_KEY the field contains the bytes of an RFC5208 ASN1
|
||||||
|
// serialized ECC public key.
|
||||||
|
optional bytes session_key = 4;
|
||||||
|
// Remote attestation data which will be present in the initial license
|
||||||
|
// request for ChromeOS client devices operating in verified mode. Remote
|
||||||
|
// attestation challenge data is |msg| field above. Optional.
|
||||||
|
optional bytes remote_attestation = 5;
|
||||||
|
|
||||||
|
repeated MetricData metric_data = 6;
|
||||||
|
// Version information from the SDK and license service. This information is
|
||||||
|
// provided in the license response.
|
||||||
|
optional VersionInfo service_version_info = 7;
|
||||||
|
// Optional field that contains the algorithm type used to generate the
|
||||||
|
// session_key and signature in a LICENSE message.
|
||||||
|
optional SessionKeyType session_key_type = 8 [default = WRAPPED_AES_KEY];
|
||||||
|
// The core message is the simple serialization of fields used by OEMCrypto.
|
||||||
|
// This field was introduced in OEMCrypto API v16.
|
||||||
|
optional bytes oemcrypto_core_message = 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HashAlgorithmProto {
|
||||||
|
// Unspecified hash algorithm: SHA_256 shall be used for ECC based algorithms
|
||||||
|
// and SHA_1 shall be used otherwise.
|
||||||
|
HASH_ALGORITHM_UNSPECIFIED = 0;
|
||||||
|
HASH_ALGORITHM_SHA_1 = 1;
|
||||||
|
HASH_ALGORITHM_SHA_256 = 2;
|
||||||
|
HASH_ALGORITHM_SHA_384 = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientIdentification message used to authenticate the client device.
|
||||||
|
message ClientIdentification {
|
||||||
|
enum TokenType {
|
||||||
|
KEYBOX = 0;
|
||||||
|
DRM_DEVICE_CERTIFICATE = 1;
|
||||||
|
REMOTE_ATTESTATION_CERTIFICATE = 2;
|
||||||
|
OEM_DEVICE_CERTIFICATE = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message NameValue {
|
||||||
|
optional string name = 1;
|
||||||
|
optional string value = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capabilities which not all clients may support. Used for the license
|
||||||
|
// exchange protocol only.
|
||||||
|
message ClientCapabilities {
|
||||||
|
enum HdcpVersion {
|
||||||
|
HDCP_NONE = 0;
|
||||||
|
HDCP_V1 = 1;
|
||||||
|
HDCP_V2 = 2;
|
||||||
|
HDCP_V2_1 = 3;
|
||||||
|
HDCP_V2_2 = 4;
|
||||||
|
HDCP_V2_3 = 5;
|
||||||
|
HDCP_NO_DIGITAL_OUTPUT = 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CertificateKeyType {
|
||||||
|
RSA_2048 = 0;
|
||||||
|
RSA_3072 = 1;
|
||||||
|
ECC_SECP256R1 = 2;
|
||||||
|
ECC_SECP384R1 = 3;
|
||||||
|
ECC_SECP521R1 = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AnalogOutputCapabilities {
|
||||||
|
ANALOG_OUTPUT_UNKNOWN = 0;
|
||||||
|
ANALOG_OUTPUT_NONE = 1;
|
||||||
|
ANALOG_OUTPUT_SUPPORTED = 2;
|
||||||
|
ANALOG_OUTPUT_SUPPORTS_CGMS_A = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bool client_token = 1 [default = false];
|
||||||
|
optional bool session_token = 2 [default = false];
|
||||||
|
optional bool video_resolution_constraints = 3 [default = false];
|
||||||
|
optional HdcpVersion max_hdcp_version = 4 [default = HDCP_NONE];
|
||||||
|
optional uint32 oem_crypto_api_version = 5;
|
||||||
|
// Client has hardware support for protecting the usage table, such as
|
||||||
|
// storing the generation number in secure memory. For Details, see:
|
||||||
|
// Widevine Modular DRM Security Integration Guide for CENC
|
||||||
|
optional bool anti_rollback_usage_table = 6 [default = false];
|
||||||
|
// The client shall report |srm_version| if available.
|
||||||
|
optional uint32 srm_version = 7;
|
||||||
|
// A device may have SRM data, and report a version, but may not be capable
|
||||||
|
// of updating SRM data.
|
||||||
|
optional bool can_update_srm = 8 [default = false];
|
||||||
|
repeated CertificateKeyType supported_certificate_key_type = 9;
|
||||||
|
optional AnalogOutputCapabilities analog_output_capabilities = 10
|
||||||
|
[default = ANALOG_OUTPUT_UNKNOWN];
|
||||||
|
optional bool can_disable_analog_output = 11 [default = false];
|
||||||
|
// Clients can indicate a performance level supported by OEMCrypto.
|
||||||
|
// This will allow applications and providers to choose an appropriate
|
||||||
|
// quality of content to serve. Currently defined tiers are
|
||||||
|
// 1 (low), 2 (medium) and 3 (high). Any other value indicates that
|
||||||
|
// the resource rating is unavailable or reporting erroneous values
|
||||||
|
// for that device. For details see,
|
||||||
|
// Widevine Modular DRM Security Integration Guide for CENC
|
||||||
|
optional uint32 resource_rating_tier = 12 [default = 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
message ClientCredentials {
|
||||||
|
optional TokenType type = 1 [default = KEYBOX];
|
||||||
|
optional bytes token = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type of factory-provisioned device root of trust. Optional.
|
||||||
|
optional TokenType type = 1 [default = KEYBOX];
|
||||||
|
// Factory-provisioned device root of trust. Required.
|
||||||
|
optional bytes token = 2;
|
||||||
|
// Optional client information name/value pairs.
|
||||||
|
repeated NameValue client_info = 3;
|
||||||
|
// Client token generated by the content provider. Optional.
|
||||||
|
optional bytes provider_client_token = 4;
|
||||||
|
// Number of licenses received by the client to which the token above belongs.
|
||||||
|
// Only present if client_token is specified.
|
||||||
|
optional uint32 license_counter = 5;
|
||||||
|
// List of non-baseline client capabilities.
|
||||||
|
optional ClientCapabilities client_capabilities = 6;
|
||||||
|
// Serialized VmpData message. Optional.
|
||||||
|
optional bytes vmp_data = 7;
|
||||||
|
// Optional field that may contain additional provisioning credentials.
|
||||||
|
repeated ClientCredentials device_credentials = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptedClientIdentification message used to hold ClientIdentification
|
||||||
|
// messages encrypted for privacy purposes.
|
||||||
|
message EncryptedClientIdentification {
|
||||||
|
// Provider ID for which the ClientIdentifcation is encrypted (owner of
|
||||||
|
// service certificate).
|
||||||
|
optional string provider_id = 1;
|
||||||
|
// Serial number for the service certificate for which ClientIdentification is
|
||||||
|
// encrypted.
|
||||||
|
optional bytes service_certificate_serial_number = 2;
|
||||||
|
// Serialized ClientIdentification message, encrypted with the privacy key
|
||||||
|
// using AES-128-CBC with PKCS#5 padding.
|
||||||
|
optional bytes encrypted_client_id = 3;
|
||||||
|
// Initialization vector needed to decrypt encrypted_client_id.
|
||||||
|
optional bytes encrypted_client_id_iv = 4;
|
||||||
|
// AES-128 privacy key, encrypted with the service public key using RSA-OAEP.
|
||||||
|
optional bytes encrypted_privacy_key = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DRM certificate definition for user devices, intermediate, service, and root
|
||||||
|
// certificates.
|
||||||
|
message DrmCertificate {
|
||||||
|
enum Type {
|
||||||
|
ROOT = 0; // ProtoBestPractices: ignore.
|
||||||
|
DEVICE_MODEL = 1;
|
||||||
|
DEVICE = 2;
|
||||||
|
SERVICE = 3;
|
||||||
|
PROVISIONER = 4;
|
||||||
|
}
|
||||||
|
enum ServiceType {
|
||||||
|
UNKNOWN_SERVICE_TYPE = 0;
|
||||||
|
LICENSE_SERVER_SDK = 1;
|
||||||
|
LICENSE_SERVER_PROXY_SDK = 2;
|
||||||
|
PROVISIONING_SDK = 3;
|
||||||
|
CAS_PROXY_SDK = 4;
|
||||||
|
}
|
||||||
|
enum Algorithm {
|
||||||
|
UNKNOWN_ALGORITHM = 0;
|
||||||
|
RSA = 1;
|
||||||
|
ECC_SECP256R1 = 2;
|
||||||
|
ECC_SECP384R1 = 3;
|
||||||
|
ECC_SECP521R1 = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EncryptionKey {
|
||||||
|
// Device public key. PKCS#1 ASN.1 DER-encoded. Required.
|
||||||
|
optional bytes public_key = 1;
|
||||||
|
// Required. The algorithm field contains the curve used to create the
|
||||||
|
// |public_key| if algorithm is one of the ECC types.
|
||||||
|
// The |algorithm| is used for both to determine the if the certificate is
|
||||||
|
// ECC or RSA. The |algorithm| also specifies the parameters that were used
|
||||||
|
// to create |public_key| and are used to create an ephemeral session key.
|
||||||
|
optional Algorithm algorithm = 2 [default = RSA];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type of certificate. Required.
|
||||||
|
optional Type type = 1;
|
||||||
|
// 128-bit globally unique serial number of certificate.
|
||||||
|
// Value is 0 for root certificate. Required.
|
||||||
|
optional bytes serial_number = 2;
|
||||||
|
// POSIX time, in seconds, when the certificate was created. Required.
|
||||||
|
optional uint32 creation_time_seconds = 3;
|
||||||
|
// POSIX time, in seconds, when the certificate should expire. Value of zero
|
||||||
|
// denotes indefinite expiry time. For more information on limited lifespan
|
||||||
|
// DRM certificates see (go/limited-lifespan-drm-certificates).
|
||||||
|
optional uint32 expiration_time_seconds = 12;
|
||||||
|
// Device public key. PKCS#1 ASN.1 DER-encoded. Required.
|
||||||
|
optional bytes public_key = 4;
|
||||||
|
// Widevine system ID for the device. Required for intermediate and
|
||||||
|
// user device certificates.
|
||||||
|
optional uint32 system_id = 5;
|
||||||
|
// Deprecated field, which used to indicate whether the device was a test
|
||||||
|
// (non-production) device. The test_device field in ProvisionedDeviceInfo
|
||||||
|
// below should be observed instead.
|
||||||
|
optional bool test_device_deprecated = 6 [deprecated = true];
|
||||||
|
// Service identifier (web origin) for the provider which owns the
|
||||||
|
// certificate. Required for service and provisioner certificates.
|
||||||
|
optional string provider_id = 7;
|
||||||
|
// This field is used only when type = SERVICE to specify which SDK uses
|
||||||
|
// service certificate. This repeated field is treated as a set. A certificate
|
||||||
|
// may be used for the specified service SDK if the appropriate ServiceType
|
||||||
|
// is specified in this field.
|
||||||
|
repeated ServiceType service_types = 8;
|
||||||
|
// Required. The algorithm field contains the curve used to create the
|
||||||
|
// |public_key| if algorithm is one of the ECC types.
|
||||||
|
// The |algorithm| is used for both to determine the if the certificate is ECC
|
||||||
|
// or RSA. The |algorithm| also specifies the parameters that were used to
|
||||||
|
// create |public_key| and are used to create an ephemeral session key.
|
||||||
|
optional Algorithm algorithm = 9 [default = RSA];
|
||||||
|
// Optional. May be present in DEVICE certificate types. This is the root
|
||||||
|
// of trust identifier that holds an encrypted value that identifies the
|
||||||
|
// keybox or other root of trust that was used to provision a DEVICE drm
|
||||||
|
// certificate.
|
||||||
|
optional bytes rot_id = 10;
|
||||||
|
// Optional. May be present in devices that explicitly support dual keys. When
|
||||||
|
// present the |public_key| is used for verification of received license
|
||||||
|
// request messages.
|
||||||
|
optional EncryptionKey encryption_key = 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrmCertificate signed by a higher (CA) DRM certificate.
|
||||||
|
message SignedDrmCertificate {
|
||||||
|
// Serialized certificate. Required.
|
||||||
|
optional bytes drm_certificate = 1;
|
||||||
|
// Signature of certificate. Signed with root or intermediate
|
||||||
|
// certificate specified below. Required.
|
||||||
|
optional bytes signature = 2;
|
||||||
|
// SignedDrmCertificate used to sign this certificate.
|
||||||
|
optional SignedDrmCertificate signer = 3;
|
||||||
|
// Optional field that indicates the hash algorithm used in signature scheme.
|
||||||
|
optional HashAlgorithmProto hash_algorithm = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message WidevinePsshData {
|
||||||
|
enum Type {
|
||||||
|
SINGLE = 0; // Single PSSH to be used to retrieve content keys.
|
||||||
|
ENTITLEMENT = 1; // Primary PSSH used to retrieve entitlement keys.
|
||||||
|
ENTITLED_KEY = 2; // Secondary PSSH containing entitled key(s).
|
||||||
|
}
|
||||||
|
|
||||||
|
message EntitledKey {
|
||||||
|
// ID of entitlement key used for wrapping |key|.
|
||||||
|
optional bytes entitlement_key_id = 1;
|
||||||
|
// ID of the entitled key.
|
||||||
|
optional bytes key_id = 2;
|
||||||
|
// Wrapped key. Required.
|
||||||
|
optional bytes key = 3;
|
||||||
|
// IV used for wrapping |key|. Required.
|
||||||
|
optional bytes iv = 4;
|
||||||
|
// Size of entitlement key used for wrapping |key|.
|
||||||
|
optional uint32 entitlement_key_size_bytes = 5 [default = 32];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entitlement or content key IDs. Can onnly present in SINGLE or ENTITLEMENT
|
||||||
|
// PSSHs. May be repeated to facilitate delivery of multiple keys in a
|
||||||
|
// single license. Cannot be used in conjunction with content_id or
|
||||||
|
// group_ids, which are the preferred mechanism.
|
||||||
|
repeated bytes key_ids = 2;
|
||||||
|
|
||||||
|
// Content identifier which may map to multiple entitlement or content key
|
||||||
|
// IDs to facilitate the delivery of multiple keys in a single license.
|
||||||
|
// Cannot be present in conjunction with key_ids, but if used must be in all
|
||||||
|
// PSSHs.
|
||||||
|
optional bytes content_id = 4;
|
||||||
|
|
||||||
|
// Crypto period index, for media using key rotation. Always corresponds to
|
||||||
|
// The content key period. This means that if using entitlement licensing
|
||||||
|
// the ENTITLED_KEY PSSHs will have sequential crypto_period_index's, whereas
|
||||||
|
// the ENTITELEMENT PSSHs will have gaps in the sequence. Required if doing
|
||||||
|
// key rotation.
|
||||||
|
optional uint32 crypto_period_index = 7;
|
||||||
|
|
||||||
|
// Protection scheme identifying the encryption algorithm. The protection
|
||||||
|
// scheme is represented as a uint32 value. The uint32 contains 4 bytes each
|
||||||
|
// representing a single ascii character in one of the 4CC protection scheme
|
||||||
|
// values. To be deprecated in favor of signaling from content.
|
||||||
|
// 'cenc' (AES-CTR) protection_scheme = 0x63656E63,
|
||||||
|
// 'cbc1' (AES-CBC) protection_scheme = 0x63626331,
|
||||||
|
// 'cens' (AES-CTR pattern encryption) protection_scheme = 0x63656E73,
|
||||||
|
// 'cbcs' (AES-CBC pattern encryption) protection_scheme = 0x63626373.
|
||||||
|
optional uint32 protection_scheme = 9;
|
||||||
|
|
||||||
|
// Optional. For media using key rotation, this represents the duration
|
||||||
|
// of each crypto period in seconds.
|
||||||
|
optional uint32 crypto_period_seconds = 10;
|
||||||
|
|
||||||
|
// Type of PSSH. Required if not SINGLE.
|
||||||
|
optional Type type = 11 [default = SINGLE];
|
||||||
|
|
||||||
|
// Key sequence for Widevine-managed keys. Optional.
|
||||||
|
optional uint32 key_sequence = 12;
|
||||||
|
|
||||||
|
// Group identifiers for all groups to which the content belongs. This can
|
||||||
|
// be used to deliver licenses to unlock multiple titles / channels.
|
||||||
|
// Optional, and may only be present in ENTITLEMENT and ENTITLED_KEY PSSHs, and
|
||||||
|
// not in conjunction with key_ids.
|
||||||
|
repeated bytes group_ids = 13;
|
||||||
|
|
||||||
|
// Copy/copies of the content key used to decrypt the media stream in which
|
||||||
|
// the PSSH box is embedded, each wrapped with a different entitlement key.
|
||||||
|
// May also contain sub-licenses to support devices with OEMCrypto 13 or
|
||||||
|
// older. May be repeated if using group entitlement keys. Present only in
|
||||||
|
// PSSHs of type ENTITLED_KEY.
|
||||||
|
repeated EntitledKey entitled_keys = 14;
|
||||||
|
|
||||||
|
// Video feature identifier, which is used in conjunction with |content_id|
|
||||||
|
// to determine the set of keys to be returned in the license. Cannot be
|
||||||
|
// present in conjunction with |key_ids|.
|
||||||
|
// Current values are "HDR".
|
||||||
|
optional string video_feature = 15;
|
||||||
|
|
||||||
|
//////////////////////////// Deprecated Fields ////////////////////////////
|
||||||
|
enum Algorithm {
|
||||||
|
UNENCRYPTED = 0;
|
||||||
|
AESCTR = 1;
|
||||||
|
};
|
||||||
|
optional Algorithm algorithm = 1 [deprecated = true];
|
||||||
|
|
||||||
|
// Content provider name.
|
||||||
|
optional string provider = 3 [deprecated = true];
|
||||||
|
|
||||||
|
// Track type. Acceptable values are SD, HD and AUDIO. Used to
|
||||||
|
// differentiate content keys used by an asset.
|
||||||
|
optional string track_type = 5 [deprecated = true];
|
||||||
|
|
||||||
|
// The name of a registered policy to be used for this asset.
|
||||||
|
optional string policy = 6 [deprecated = true];
|
||||||
|
|
||||||
|
// Optional protected context for group content. The grouped_license is a
|
||||||
|
// serialized SignedMessage.
|
||||||
|
optional bytes grouped_license = 8 [deprecated = true];
|
||||||
|
}
|
||||||
|
|
||||||
|
// File Hashes for Verified Media Path (VMP) support.
|
||||||
|
message FileHashes {
|
||||||
|
message Signature {
|
||||||
|
optional string filename = 1;
|
||||||
|
optional bool test_signing = 2; //0 - release, 1 - testing
|
||||||
|
optional bytes SHA512Hash = 3;
|
||||||
|
optional bool main_exe = 4; //0 for dlls, 1 for exe, this is field 3 in file
|
||||||
|
optional bytes signature = 5;
|
||||||
|
}
|
||||||
|
optional bytes signer = 1;
|
||||||
|
repeated Signature signatures = 2;
|
||||||
|
}
|
||||||
4996
modules/license_protocol.ts
Normal file
4996
modules/license_protocol.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,8 @@ const domain = {
|
||||||
www_beta: 'https://beta.crunchyroll.com',
|
www_beta: 'https://beta.crunchyroll.com',
|
||||||
api_beta: 'https://beta-api.crunchyroll.com',
|
api_beta: 'https://beta-api.crunchyroll.com',
|
||||||
hd_www: 'https://www.hidive.com',
|
hd_www: 'https://www.hidive.com',
|
||||||
hd_api: 'https://api.hidive.com'
|
hd_api: 'https://api.hidive.com',
|
||||||
|
hd_new: 'https://dce-frontoffice.imggaming.com'
|
||||||
};
|
};
|
||||||
|
|
||||||
export type APIType = {
|
export type APIType = {
|
||||||
|
|
@ -41,6 +42,9 @@ export type APIType = {
|
||||||
hd_clientWeb: string,
|
hd_clientWeb: string,
|
||||||
hd_clientExo: string,
|
hd_clientExo: string,
|
||||||
hd_api: string,
|
hd_api: string,
|
||||||
|
hd_new_api: string,
|
||||||
|
hd_new_apiKey: string,
|
||||||
|
hd_new_version: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// api urls
|
// api urls
|
||||||
|
|
@ -77,6 +81,10 @@ const api: APIType = {
|
||||||
hd_clientWeb: 'okhttp/3.4.1',
|
hd_clientWeb: 'okhttp/3.4.1',
|
||||||
hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0',
|
hd_clientExo: 'smartexoplayer/1.6.0.R (Linux;Android 6.0) ExoPlayerLib/2.6.0',
|
||||||
hd_api: `${domain.hd_api}/api/v1`,
|
hd_api: `${domain.hd_api}/api/v1`,
|
||||||
|
//Hidive New API
|
||||||
|
hd_new_api: `${domain.hd_new}/api`,
|
||||||
|
hd_new_apiKey: '857a1e5d-e35e-4fdf-805b-a87b6f8364bf',
|
||||||
|
hd_new_version: '6.0.1.bbf09a2'
|
||||||
};
|
};
|
||||||
|
|
||||||
// set header
|
// set header
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ let argvC: {
|
||||||
new: boolean | undefined;
|
new: boolean | undefined;
|
||||||
'movie-listing': string | undefined;
|
'movie-listing': string | undefined;
|
||||||
series: string | undefined;
|
series: string | undefined;
|
||||||
s: string | undefined;
|
s: string | undefined;
|
||||||
|
srz: string | undefined;
|
||||||
e: string | undefined;
|
e: string | undefined;
|
||||||
extid: string | undefined;
|
extid: string | undefined;
|
||||||
q: number;
|
q: number;
|
||||||
|
|
@ -64,6 +65,9 @@ let argvC: {
|
||||||
_: (string | number)[];
|
_: (string | number)[];
|
||||||
$0: string;
|
$0: string;
|
||||||
dlVideoOnce: boolean;
|
dlVideoOnce: boolean;
|
||||||
|
chapters: boolean;
|
||||||
|
crapi: 'android' | 'web';
|
||||||
|
hdapi: 'old' | 'new';
|
||||||
removeBumpers: boolean;
|
removeBumpers: boolean;
|
||||||
originalFontSize: boolean;
|
originalFontSize: boolean;
|
||||||
keepAllVideos: boolean;
|
keepAllVideos: boolean;
|
||||||
|
|
|
||||||
|
|
@ -138,8 +138,7 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
group: 'dl',
|
group: 'dl',
|
||||||
alias: 'srz',
|
alias: 'srz',
|
||||||
describe: 'Get season list by series ID',
|
describe: 'Get season list by series ID',
|
||||||
docDescribe: 'This command is used only for crunchyroll.'
|
docDescribe: 'Requested is the ID of a show not a season.',
|
||||||
+ '\n Requested is the ID of a show not a season.',
|
|
||||||
service: ['crunchy'],
|
service: ['crunchy'],
|
||||||
type: 'string',
|
type: 'string',
|
||||||
usage: '${ID}'
|
usage: '${ID}'
|
||||||
|
|
@ -203,6 +202,47 @@ const args: TAppArg<boolean|number|string|unknown[]>[] = [
|
||||||
default: false
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'chapters',
|
||||||
|
describe: 'Will fetch the chapters and add them into the final video',
|
||||||
|
type: 'boolean',
|
||||||
|
group: 'dl',
|
||||||
|
service: ['crunchy'],
|
||||||
|
docDescribe: 'Will fetch the chapters and add them into the final video.'
|
||||||
|
+ '\nCurrently only works with mkvmerge.',
|
||||||
|
usage: '',
|
||||||
|
default: {
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'crapi',
|
||||||
|
describe: 'Selects the API type for Crunchyroll',
|
||||||
|
type: 'string',
|
||||||
|
group: 'dl',
|
||||||
|
service: ['crunchy'],
|
||||||
|
docDescribe: 'If set to Android, it has lower quality, but Non-DRM streams,'
|
||||||
|
+ '\nIf set to Web, it has a higher quality adaptive stream, but everything is DRM.',
|
||||||
|
usage: '',
|
||||||
|
choices: ['android', 'web'],
|
||||||
|
default: {
|
||||||
|
default: 'android'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hdapi',
|
||||||
|
describe: 'Selects the API type for Hidive',
|
||||||
|
type: 'string',
|
||||||
|
group: 'dl',
|
||||||
|
service: ['hidive'],
|
||||||
|
docDescribe: 'If set to Old, it has lower quality, but Non-DRM streams, but some people can\'t use it,'
|
||||||
|
+ '\nIf set to New, it has a higher quality stream, but everything is DRM.',
|
||||||
|
usage: '',
|
||||||
|
choices: ['old', 'new'],
|
||||||
|
default: {
|
||||||
|
default: 'old'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'removeBumpers',
|
name: 'removeBumpers',
|
||||||
describe: 'Remove bumpers from final video',
|
describe: 'Remove bumpers from final video',
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ const stateFile = path.join(workingDir, 'config', 'guistate');
|
||||||
const tokenFile = {
|
const tokenFile = {
|
||||||
funi: path.join(workingDir, 'config', 'funi_token'),
|
funi: path.join(workingDir, 'config', 'funi_token'),
|
||||||
cr: path.join(workingDir, 'config', 'cr_token'),
|
cr: path.join(workingDir, 'config', 'cr_token'),
|
||||||
hd: path.join(workingDir, 'config', 'hd_token')
|
hd: path.join(workingDir, 'config', 'hd_token'),
|
||||||
|
hdNew: path.join(workingDir, 'config', 'hd_new_token')
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ensureConfig = () => {
|
export const ensureConfig = () => {
|
||||||
|
|
@ -82,7 +83,8 @@ export type ConfigObject = {
|
||||||
bin: {
|
bin: {
|
||||||
ffmpeg?: string,
|
ffmpeg?: string,
|
||||||
mkvmerge?: string,
|
mkvmerge?: string,
|
||||||
ffprobe?: string
|
ffprobe?: string,
|
||||||
|
mp4decrypt?: string
|
||||||
},
|
},
|
||||||
cli: {
|
cli: {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
|
|
@ -146,7 +148,8 @@ const loadBinCfg = async () => {
|
||||||
const defaultBin = {
|
const defaultBin = {
|
||||||
ffmpeg: 'ffmpeg',
|
ffmpeg: 'ffmpeg',
|
||||||
mkvmerge: 'mkvmerge',
|
mkvmerge: 'mkvmerge',
|
||||||
ffprobe: 'ffprobe'
|
ffprobe: 'ffprobe',
|
||||||
|
mp4decrypt: 'mp4decrypt'
|
||||||
};
|
};
|
||||||
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
|
const keys = Object.keys(defaultBin) as (keyof typeof defaultBin)[];
|
||||||
for(const dir of keys){
|
for(const dir of keys){
|
||||||
|
|
@ -240,7 +243,7 @@ const saveHDSession = (data: Record<string, unknown>) => {
|
||||||
|
|
||||||
|
|
||||||
const loadHDToken = () => {
|
const loadHDToken = () => {
|
||||||
let token = loadYamlCfgFile(tokenFile.cr, true);
|
let token = loadYamlCfgFile(tokenFile.hd, true);
|
||||||
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||||
token = {};
|
token = {};
|
||||||
}
|
}
|
||||||
|
|
@ -290,6 +293,25 @@ const loadHDProfile = () => {
|
||||||
return profile;
|
return profile;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadNewHDToken = () => {
|
||||||
|
let token = loadYamlCfgFile(tokenFile.hdNew, true);
|
||||||
|
if(typeof token !== 'object' || token === null || Array.isArray(token)){
|
||||||
|
token = {};
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveNewHDToken = (data: Record<string, unknown>) => {
|
||||||
|
const cfgFolder = path.dirname(tokenFile.hdNew);
|
||||||
|
try{
|
||||||
|
fs.ensureDirSync(cfgFolder);
|
||||||
|
fs.writeFileSync(`${tokenFile.hdNew}.yml`, yaml.stringify(data));
|
||||||
|
}
|
||||||
|
catch(e){
|
||||||
|
console.error('Can\'t save token file to disk!');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadFuniToken = () => {
|
const loadFuniToken = () => {
|
||||||
const loadedToken = loadYamlCfgFile<{
|
const loadedToken = loadYamlCfgFile<{
|
||||||
token?: string
|
token?: string
|
||||||
|
|
@ -361,6 +383,8 @@ export {
|
||||||
loadHDSession,
|
loadHDSession,
|
||||||
saveHDToken,
|
saveHDToken,
|
||||||
loadHDToken,
|
loadHDToken,
|
||||||
|
saveNewHDToken,
|
||||||
|
loadNewHDToken,
|
||||||
saveHDProfile,
|
saveHDProfile,
|
||||||
loadHDProfile,
|
loadHDProfile,
|
||||||
getState,
|
getState,
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
export type LanguageItem = {
|
export type LanguageItem = {
|
||||||
cr_locale?: string,
|
cr_locale?: string,
|
||||||
hd_locale?: string,
|
hd_locale?: string,
|
||||||
|
new_hd_locale?: string,
|
||||||
locale: string,
|
locale: string,
|
||||||
code: string,
|
code: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -13,12 +14,12 @@ export type LanguageItem = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const languages: LanguageItem[] = [
|
const languages: LanguageItem[] = [
|
||||||
{ cr_locale: 'en-US', hd_locale: 'English', funi_locale: 'enUS', locale: 'en', code: 'eng', name: 'English' },
|
{ cr_locale: 'en-US', new_hd_locale: 'en-US', hd_locale: 'English', funi_locale: 'enUS', locale: 'en', code: 'eng', name: 'English' },
|
||||||
{ cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', },
|
{ cr_locale: 'en-IN', locale: 'en-IN', code: 'eng', name: 'English (India)', },
|
||||||
{ cr_locale: 'es-LA', hd_locale: 'Spanish LatAm', funi_name: 'Spanish (LAS)', funi_name_lagacy: 'Spanish (Latin Am)', funi_locale: 'esLA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
|
{ cr_locale: 'es-LA', new_hd_locale: 'es-MX', hd_locale: 'Spanish LatAm', funi_name: 'Spanish (LAS)', funi_name_lagacy: 'Spanish (Latin Am)', funi_locale: 'esLA', locale: 'es-419', code: 'spa', name: 'Spanish', language: 'Latin American Spanish' },
|
||||||
{ cr_locale: 'es-419',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
|
{ cr_locale: 'es-419',hd_locale: 'Spanish', locale: 'es-419', code: 'spa-419', name: 'Spanish', language: 'Latin American Spanish' },
|
||||||
{ cr_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' },
|
{ cr_locale: 'es-ES', hd_locale: 'Spanish Europe', locale: 'es-ES', code: 'spa-ES', name: 'Castilian', language: 'European Spanish' },
|
||||||
{ cr_locale: 'pt-BR', hd_locale: 'Portuguese', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
|
{ cr_locale: 'pt-BR', new_hd_locale: 'pt-BR', hd_locale: 'Portuguese', funi_name: 'Portuguese (Brazil)', funi_locale: 'ptBR', locale: 'pt-BR', code: 'por', name: 'Portuguese', language: 'Brazilian Portuguese' },
|
||||||
{ cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' },
|
{ cr_locale: 'pt-PT', locale: 'pt-PT', code: 'por', name: 'Portuguese (Portugal)', language: 'Portugues (Portugal)' },
|
||||||
{ cr_locale: 'fr-FR', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
{ cr_locale: 'fr-FR', hd_locale: 'French', locale: 'fr', code: 'fra', name: 'French' },
|
||||||
{ cr_locale: 'de-DE', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
{ cr_locale: 'de-DE', hd_locale: 'German', locale: 'de', code: 'deu', name: 'German' },
|
||||||
|
|
@ -142,9 +143,9 @@ const sortTags = (data: string[]) => {
|
||||||
return sort.map(e => e.locale as string);
|
return sort.map(e => e.locale as string);
|
||||||
};
|
};
|
||||||
|
|
||||||
const subsFile = (fnOutput:string, subsIndex: string, langItem: LanguageItem, isCC: boolean, ccTag: string) => {
|
const subsFile = (fnOutput:string, subsIndex: string, langItem: LanguageItem, isCC: boolean, ccTag: string, isSigns?: boolean, format?: string) => {
|
||||||
subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0');
|
subsIndex = (parseInt(subsIndex) + 1).toString().padStart(2, '0');
|
||||||
return `${fnOutput}.${subsIndex}.${langItem.code}.${langItem.language}${isCC ? `.${ccTag}` : ''}.ass`;
|
return `${fnOutput}.${subsIndex}.${langItem.code}.${langItem.language}${isCC ? `.${ccTag}` : ''}${isSigns ? '.signs' : ''}.${format ? format : 'ass'}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
// construct dub langs const
|
// construct dub langs const
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export type SubtitleInput = {
|
||||||
language: LanguageItem,
|
language: LanguageItem,
|
||||||
file: string,
|
file: string,
|
||||||
closedCaption?: boolean,
|
closedCaption?: boolean,
|
||||||
|
signs?: boolean,
|
||||||
delay?: number
|
delay?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,6 +38,7 @@ export type MergerOptions = {
|
||||||
onlyVid: MergerInput[],
|
onlyVid: MergerInput[],
|
||||||
onlyAudio: MergerInput[],
|
onlyAudio: MergerInput[],
|
||||||
subtitles: SubtitleInput[],
|
subtitles: SubtitleInput[],
|
||||||
|
chapters?: MergerInput[],
|
||||||
ccTag: string,
|
ccTag: string,
|
||||||
output: string,
|
output: string,
|
||||||
videoTitle?: string,
|
videoTitle?: string,
|
||||||
|
|
@ -162,7 +164,7 @@ class Merger {
|
||||||
args.push(`-i "${sub.file}"`);
|
args.push(`-i "${sub.file}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.output.split('.').pop() === 'mkv')
|
if (this.options.output.split('.').pop() === 'mkv') {
|
||||||
if (this.options.fonts) {
|
if (this.options.fonts) {
|
||||||
let fontIndex = 0;
|
let fontIndex = 0;
|
||||||
for (const font of this.options.fonts) {
|
for (const font of this.options.fonts) {
|
||||||
|
|
@ -170,6 +172,9 @@ class Merger {
|
||||||
fontIndex++;
|
fontIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: Make it possible for chapters to work with ffmpeg merging
|
||||||
|
|
||||||
args.push(...metaData);
|
args.push(...metaData);
|
||||||
args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
|
args.push(...this.options.subtitles.map((_, subIndex) => `-map ${subIndex + index}`));
|
||||||
|
|
@ -178,7 +183,7 @@ class Merger {
|
||||||
'-c:a copy',
|
'-c:a copy',
|
||||||
this.options.output.split('.').pop()?.toLowerCase() === 'mp4' ? '-c:s mov_text' : '-c:s ass',
|
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="${
|
...this.options.subtitles.map((sub, subindex) => `-metadata:s:s:${subindex} title="${
|
||||||
(sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ` ${this.options.ccTag}` : ''}`
|
(sub.language.language || sub.language.name) + `${sub.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${sub.signs === true ? ' Signs' : ''}`
|
||||||
}" -metadata:s:s:${subindex} language=${sub.language.code}`)
|
}" -metadata:s:s:${subindex} language=${sub.language.code}`)
|
||||||
);
|
);
|
||||||
args.push(...this.options.options.ffmpeg);
|
args.push(...this.options.options.ffmpeg);
|
||||||
|
|
@ -281,7 +286,7 @@ class Merger {
|
||||||
`--sync 0:-${Math.ceil(subObj.delay*1000)}`
|
`--sync 0:-${Math.ceil(subObj.delay*1000)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}`}"`);
|
args.push('--track-name', `0:"${(subObj.language.language || subObj.language.name) + `${subObj.closedCaption === true ? ` ${this.options.ccTag}` : ''}` + `${subObj.signs === true ? ' Signs' : ''}`}"`);
|
||||||
args.push('--language', `0:"${subObj.language.code}"`);
|
args.push('--language', `0:"${subObj.language.code}"`);
|
||||||
//TODO: look into making Closed Caption default if it's the only sub of the default language downloaded
|
//TODO: look into making Closed Caption default if it's the only sub of the default language downloaded
|
||||||
if (this.options.defaults.sub.code === subObj.language.code && !subObj.closedCaption) {
|
if (this.options.defaults.sub.code === subObj.language.code && !subObj.closedCaption) {
|
||||||
|
|
@ -296,6 +301,7 @@ class Merger {
|
||||||
'--no-subtitles',
|
'--no-subtitles',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.options.fonts && this.options.fonts.length > 0) {
|
if (this.options.fonts && this.options.fonts.length > 0) {
|
||||||
for (const f of this.options.fonts) {
|
for (const f of this.options.fonts) {
|
||||||
args.push('--attachment-name', f.name);
|
args.push('--attachment-name', f.name);
|
||||||
|
|
@ -308,6 +314,10 @@ class Merger {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.chapters && this.options.chapters.length > 0) {
|
||||||
|
args.push(`--chapters "${this.options.chapters[0].path}"`);
|
||||||
|
}
|
||||||
|
|
||||||
return args.join(' ');
|
return args.join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -405,6 +415,7 @@ class Merger {
|
||||||
|
|
||||||
public cleanUp() {
|
public cleanUp() {
|
||||||
this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path));
|
this.options.onlyAudio.concat(this.options.onlyVid).concat(this.options.videoAndAudio).forEach(a => fs.unlinkSync(a.path));
|
||||||
|
this.options.chapters?.forEach(a => fs.unlinkSync(a.path));
|
||||||
this.options.subtitles.forEach(a => fs.unlinkSync(a.file));
|
this.options.subtitles.forEach(a => fs.unlinkSync(a.file));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@ class Req {
|
||||||
options.headers = {...options.headers, ...params.headers};
|
options.headers = {...options.headers, ...params.headers};
|
||||||
}
|
}
|
||||||
if(options.method == 'POST'){
|
if(options.method == 'POST'){
|
||||||
(options.headers as Headers)['Content-Type'] = 'application/x-www-form-urlencoded';
|
if (!(options.headers as Headers)['Content-Type']) {
|
||||||
|
(options.headers as Headers)['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(params.body){
|
if(params.body){
|
||||||
options.body = params.body;
|
options.body = params.body;
|
||||||
|
|
|
||||||
122
modules/module.transform-mpd.ts
Normal file
122
modules/module.transform-mpd.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
import { parse as mpdParse } from 'mpd-parser';
|
||||||
|
import { LanguageItem, findLang, languages } from './module.langsData';
|
||||||
|
|
||||||
|
type Segment = {
|
||||||
|
uri: string;
|
||||||
|
timeline: number;
|
||||||
|
duration: number;
|
||||||
|
map: {
|
||||||
|
uri: string;
|
||||||
|
};
|
||||||
|
number: number;
|
||||||
|
presentationTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlaylistItem = {
|
||||||
|
pssh?: string,
|
||||||
|
bandwidth: number,
|
||||||
|
segments: Segment[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type AudioPlayList = {
|
||||||
|
language: LanguageItem,
|
||||||
|
default: boolean
|
||||||
|
} & PlaylistItem
|
||||||
|
|
||||||
|
type VideoPlayList = {
|
||||||
|
quality: {
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
} & PlaylistItem
|
||||||
|
|
||||||
|
export type MPDParsed = {
|
||||||
|
[server: string]: {
|
||||||
|
audio: AudioPlayList[],
|
||||||
|
video: VideoPlayList[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parse(manifest: string, language?: LanguageItem, url?: string) {
|
||||||
|
if (!manifest.includes('BaseURL') && url) {
|
||||||
|
manifest = manifest.replace(/(<MPD*\b[^>]*>)/gm, `$1<BaseURL>${url}</BaseURL>`);
|
||||||
|
}
|
||||||
|
const parsed = mpdParse(manifest);
|
||||||
|
const ret: MPDParsed = {};
|
||||||
|
|
||||||
|
for (const item of Object.values(parsed.mediaGroups.AUDIO.audio)){
|
||||||
|
for (const playlist of item.playlists) {
|
||||||
|
const host = new URL(playlist.resolvedUri).hostname;
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(ret, host))
|
||||||
|
ret[host] = { audio: [], video: [] };
|
||||||
|
|
||||||
|
//Find and add audio language if it is found in the MPD
|
||||||
|
let audiolang: LanguageItem;
|
||||||
|
const foundlanguage = findLang(languages.find(a => a.code === item.language)?.cr_locale ?? 'unknown');
|
||||||
|
if (item.language) {
|
||||||
|
audiolang = foundlanguage;
|
||||||
|
} else {
|
||||||
|
audiolang = language ? language : foundlanguage;
|
||||||
|
}
|
||||||
|
const pItem: AudioPlayList = {
|
||||||
|
bandwidth: playlist.attributes.BANDWIDTH,
|
||||||
|
language: audiolang,
|
||||||
|
default: item.default,
|
||||||
|
segments: playlist.segments.map((segment): Segment => {
|
||||||
|
const uri = segment.resolvedUri;
|
||||||
|
const map_uri = segment.map.resolvedUri;
|
||||||
|
return {
|
||||||
|
duration: segment.duration,
|
||||||
|
map: { uri: map_uri },
|
||||||
|
number: segment.number,
|
||||||
|
presentationTime: segment.presentationTime,
|
||||||
|
timeline: segment.timeline,
|
||||||
|
uri
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (playlist.contentProtection &&
|
||||||
|
playlist.contentProtection?.['com.widevine.alpha'].pssh)
|
||||||
|
pItem.pssh = arrayBufferToBase64(playlist.contentProtection['com.widevine.alpha'].pssh);
|
||||||
|
|
||||||
|
ret[host].audio.push(pItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const playlist of parsed.playlists) {
|
||||||
|
const host = new URL(playlist.resolvedUri).hostname;
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(ret, host))
|
||||||
|
ret[host] = { audio: [], video: [] };
|
||||||
|
|
||||||
|
const pItem: VideoPlayList = {
|
||||||
|
bandwidth: playlist.attributes.BANDWIDTH,
|
||||||
|
quality: playlist.attributes.RESOLUTION!,
|
||||||
|
segments: playlist.segments.map((segment): Segment => {
|
||||||
|
const uri = segment.resolvedUri;
|
||||||
|
const map_uri = segment.map.resolvedUri;
|
||||||
|
return {
|
||||||
|
duration: segment.duration,
|
||||||
|
map: { uri: map_uri },
|
||||||
|
number: segment.number,
|
||||||
|
presentationTime: segment.presentationTime,
|
||||||
|
timeline: segment.timeline,
|
||||||
|
uri
|
||||||
|
};
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (playlist.contentProtection &&
|
||||||
|
playlist.contentProtection?.['com.widevine.alpha'].pssh)
|
||||||
|
pItem.pssh = arrayBufferToBase64(playlist.contentProtection['com.widevine.alpha'].pssh);
|
||||||
|
|
||||||
|
ret[host].video.push(pItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function arrayBufferToBase64(buffer: Uint8Array): string {
|
||||||
|
return Buffer.from(buffer).toString('base64');
|
||||||
|
}
|
||||||
|
|
@ -69,7 +69,7 @@ function loadCSS(cssStr: string): Css {
|
||||||
function parseStyle(stylegroup: string, line: string, style: any) {
|
function parseStyle(stylegroup: string, line: string, style: any) {
|
||||||
const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let
|
const defaultSFont = rFont == '' ? defaultStyleFont : rFont; //redeclare cause of let
|
||||||
|
|
||||||
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song')) { //base for dialog, everything else use defaultStyle
|
if (stylegroup.startsWith('Subtitle') || stylegroup.startsWith('Song') || stylegroup.startsWith('Q0') || stylegroup.startsWith('Q1')) { //base for dialog, everything else use defaultStyle
|
||||||
style = `${defaultSFont},${fontSize},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1`;
|
style = `${defaultSFont},${fontSize},&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -261,6 +261,7 @@ function convert(css: Css, vtt: Vtt[]) {
|
||||||
song_cap: [],
|
song_cap: [],
|
||||||
};
|
};
|
||||||
const linesMap: Record<string, number> = {};
|
const linesMap: Record<string, number> = {};
|
||||||
|
let previousLine: ReturnType<typeof convertLine> | undefined = undefined;
|
||||||
for (const l in vtt) {
|
for (const l in vtt) {
|
||||||
const x = convertLine(stylesMap, vtt[l]);
|
const x = convertLine(stylesMap, vtt[l]);
|
||||||
if (x.ind !== '' && linesMap[x.ind] !== undefined) {
|
if (x.ind !== '' && linesMap[x.ind] !== undefined) {
|
||||||
|
|
@ -278,7 +279,17 @@ function convert(css: Css, vtt: Vtt[]) {
|
||||||
linesMap[x.ind] = events[x.type as keyof typeof events].length - 1;
|
linesMap[x.ind] = events[x.type as keyof typeof events].length - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* What cursed code have I brought upon this land?
|
||||||
|
* This checks if a subtitle should be multi-line, and if it is, pops the just inserted
|
||||||
|
* subtitle and the previous subtitle, and merges them into a single subtitle.
|
||||||
|
*/
|
||||||
|
if (previousLine?.start == x.start && previousLine.type == x.type && previousLine.style == x.style) {
|
||||||
|
events[x.type as keyof typeof events].pop();
|
||||||
|
const previousLinePop = events[x.type as keyof typeof events].pop();
|
||||||
|
events[x.type as keyof typeof events].push(previousLinePop + '\\N'+x.text);
|
||||||
|
}
|
||||||
|
previousLine = x;
|
||||||
}
|
}
|
||||||
if (events.subtitle.length > 0) {
|
if (events.subtitle.length > 0) {
|
||||||
ass = ass.concat(
|
ass = ass.concat(
|
||||||
|
|
@ -399,6 +410,23 @@ function vtt(group: string | undefined, xFontSize: number | undefined, vttStr: s
|
||||||
fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix
|
fontSize = xFontSize && xFontSize > 0 ? xFontSize : 34; // 1em to pix
|
||||||
tmMrg = timeMargin ? timeMargin : 0; //
|
tmMrg = timeMargin ? timeMargin : 0; //
|
||||||
rFont = replaceFont ? replaceFont : rFont;
|
rFont = replaceFont ? replaceFont : rFont;
|
||||||
|
if (vttStr.match(/::cue(?:.(.+)\))?{([^}]+)}/g)) {
|
||||||
|
const cssLines = [];
|
||||||
|
let defaultCss = '';
|
||||||
|
const cssGroups = vttStr.matchAll(/::cue(?:.(.+)\))?{([^}]+)}/g);
|
||||||
|
for (const cssGroup of cssGroups) {
|
||||||
|
//Below code will bulldoze defined sizes for custom ones
|
||||||
|
/*if (!options.originalFontSize) {
|
||||||
|
cssGroup[2] = cssGroup[2].replace(/( font-size:.+?;)/g, '').replace(/(font-size:.+?;)/g, '');
|
||||||
|
}*/
|
||||||
|
if (cssGroup[1]) {
|
||||||
|
cssLines.push(`${cssGroup[1]}{${defaultCss}${cssGroup[2]}}`);
|
||||||
|
} else {
|
||||||
|
defaultCss = cssGroup[2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cssStr += cssLines.join('\r\n');
|
||||||
|
}
|
||||||
return convert(
|
return convert(
|
||||||
loadCSS(cssStr),
|
loadCSS(cssStr),
|
||||||
loadVTT(vttStr)
|
loadVTT(vttStr)
|
||||||
|
|
|
||||||
77
modules/widevine.ts
Normal file
77
modules/widevine.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { KeyContainer, Session } from './license';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { console } from './log';
|
||||||
|
import got from 'got';
|
||||||
|
import { workingDir } from './module.cfg-loader';
|
||||||
|
import path from 'path';
|
||||||
|
import { ReadError, Response } from 'got';
|
||||||
|
|
||||||
|
//read cdm files located in the same directory
|
||||||
|
let privateKey: Buffer, identifierBlob: Buffer;
|
||||||
|
export let canDecrypt: boolean;
|
||||||
|
try {
|
||||||
|
privateKey = fs.readFileSync(path.join(workingDir, 'widevine', 'device_private_key'));
|
||||||
|
identifierBlob = fs.readFileSync(path.join(workingDir, 'widevine', 'device_client_id_blob'));
|
||||||
|
canDecrypt = true;
|
||||||
|
} catch (e) {
|
||||||
|
canDecrypt = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function getKeys(pssh: string | undefined, licenseServer: string, authData: Record<string, string>): Promise<KeyContainer[]> {
|
||||||
|
if (!pssh || !canDecrypt) return [];
|
||||||
|
//pssh found in the mpd manifest
|
||||||
|
const psshBuffer = Buffer.from(
|
||||||
|
pssh,
|
||||||
|
'base64'
|
||||||
|
);
|
||||||
|
|
||||||
|
//Create a new widevine session
|
||||||
|
const session = new Session({ privateKey, identifierBlob }, psshBuffer);
|
||||||
|
|
||||||
|
//Generate license
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await got(licenseServer, {
|
||||||
|
method: 'POST',
|
||||||
|
body: session.createLicenseRequest(),
|
||||||
|
headers: authData,
|
||||||
|
responseType: 'text'
|
||||||
|
});
|
||||||
|
} catch(_error){
|
||||||
|
const error = _error as {
|
||||||
|
name: string
|
||||||
|
} & ReadError & {
|
||||||
|
res: Response<unknown>
|
||||||
|
};
|
||||||
|
if(error.response && error.response.statusCode && error.response.statusMessage){
|
||||||
|
console.error(`${error.name} ${error.response.statusCode}: ${error.response.statusMessage}`);
|
||||||
|
} else{
|
||||||
|
console.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.error(docTitle[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(error.res && error.res.body && error.response.statusCode
|
||||||
|
&& error.response.statusCode != 404 && error.response.statusCode != 403){
|
||||||
|
console.error('Body:', error.res.body);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
//Parse License and return keys
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(response.body);
|
||||||
|
return session.parseLicense(Buffer.from(json['license'], 'base64'));
|
||||||
|
} catch {
|
||||||
|
return session.parseLicense(response.rawBody);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.info('License request failed:', response.statusMessage, response.body);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
35
package.json
35
package.json
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "multi-downloader-nx",
|
"name": "multi-downloader-nx",
|
||||||
"short_name": "aniDL",
|
"short_name": "aniDL",
|
||||||
"version": "4.4.4",
|
"version": "4.5.1",
|
||||||
"description": "Downloader for Crunchyroll, Funimation, or Hidive via CLI or GUI",
|
"description": "Downloader for Crunchyroll, Funimation, or Hidive via CLI or GUI",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"download",
|
"download",
|
||||||
|
|
@ -45,6 +45,8 @@
|
||||||
"@babel/core": "^7.22.9",
|
"@babel/core": "^7.22.9",
|
||||||
"@babel/plugin-syntax-flow": "^7.22.5",
|
"@babel/plugin-syntax-flow": "^7.22.5",
|
||||||
"@babel/plugin-transform-react-jsx": "^7.22.5",
|
"@babel/plugin-transform-react-jsx": "^7.22.5",
|
||||||
|
"@types/xmldom": "^0.1.34",
|
||||||
|
"@yao-pkg/pkg": "^5.11.1",
|
||||||
"cheerio": "1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
|
|
@ -56,12 +58,16 @@
|
||||||
"got": "^11.8.6",
|
"got": "^11.8.6",
|
||||||
"iso-639": "^0.2.2",
|
"iso-639": "^0.2.2",
|
||||||
"log4js": "^6.9.1",
|
"log4js": "^6.9.1",
|
||||||
|
"long": "^5.2.3",
|
||||||
"lookpath": "^1.2.2",
|
"lookpath": "^1.2.2",
|
||||||
"m3u8-parsed": "^1.3.0",
|
"m3u8-parsed": "^1.3.0",
|
||||||
|
"mpd-parser": "^1.3.0",
|
||||||
"open": "^8.4.2",
|
"open": "^8.4.2",
|
||||||
|
"protobufjs": "^7.2.5",
|
||||||
"sei-helper": "^3.3.0",
|
"sei-helper": "^3.3.0",
|
||||||
"typescript-eslint": "0.0.1-alpha.0",
|
"typescript-eslint": "0.0.1-alpha.0",
|
||||||
"ws": "^8.13.0",
|
"ws": "^8.13.0",
|
||||||
|
"xmldom": "^0.6.0",
|
||||||
"yaml": "^2.3.1",
|
"yaml": "^2.3.1",
|
||||||
"yargs": "^17.7.2"
|
"yargs": "^17.7.2"
|
||||||
},
|
},
|
||||||
|
|
@ -69,7 +75,6 @@
|
||||||
"@types/cors": "^2.8.13",
|
"@types/cors": "^2.8.13",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/ffprobe": "^1.1.4",
|
"@types/ffprobe": "^1.1.4",
|
||||||
"@types/ffprobe-static": "^2.0.1",
|
|
||||||
"@types/fs-extra": "^11.0.1",
|
"@types/fs-extra": "^11.0.1",
|
||||||
"@types/node": "^18.15.11",
|
"@types/node": "^18.15.11",
|
||||||
"@types/ws": "^8.5.5",
|
"@types/ws": "^8.5.5",
|
||||||
|
|
@ -82,9 +87,10 @@
|
||||||
"eslint-config-react-app": "^7.0.1",
|
"eslint-config-react-app": "^7.0.1",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.27.5",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.32.2",
|
||||||
"pkg": "^5.8.1",
|
"protoc": "^1.1.3",
|
||||||
"removeNPMAbsolutePaths": "^3.0.1",
|
"removeNPMAbsolutePaths": "^3.0.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
"ts-proto": "^1.169.1",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -92,19 +98,24 @@
|
||||||
"start": "pnpm prestart && cd lib && node gui.js",
|
"start": "pnpm prestart && cd lib && node gui.js",
|
||||||
"docs": "ts-node modules/build-docs.ts",
|
"docs": "ts-node modules/build-docs.ts",
|
||||||
"tsc": "ts-node tsc.ts",
|
"tsc": "ts-node tsc.ts",
|
||||||
|
"proto:compile": "protoc --plugin=protoc-gen-ts_proto=.\\node_modules\\.bin\\protoc-gen-ts_proto.cmd --ts_proto_opt=\"esModuleInterop=true\" --ts_proto_opt=\"forceLong=long\" --ts_proto_opt=\"env=node\" --ts_proto_out=. modules/*.proto",
|
||||||
"prebuild-cli": "pnpm run tsc false false",
|
"prebuild-cli": "pnpm run tsc false false",
|
||||||
"build-windows-cli": "pnpm run prebuild-cli && cd lib && node modules/build windows64",
|
"build-windows-cli": "pnpm run prebuild-cli && cd lib && node modules/build windows-x64",
|
||||||
"build-ubuntu-cli": "pnpm run prebuild-cli && cd lib && node modules/build ubuntu64",
|
"build-linux-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-x64",
|
||||||
"build-arm-cli": "pnpm run prebuild-cli && cd lib && node modules/build arm64",
|
"build-arm-cli": "pnpm run prebuild-cli && cd lib && node modules/build linux-arm64",
|
||||||
"build-macos-cli": "pnpm run prebuild-cli && cd lib && node modules/build macos64",
|
"build-macos-cli": "pnpm run prebuild-cli && cd lib && node modules/build macos-x64",
|
||||||
|
"build-alpine-cli": "pnpm run prebuild-cli && cd lib && node modules/build alpine-x64",
|
||||||
|
"build-android-cli": "pnpm run prebuild-cli && cd lib && node modules/build linuxstatic-armv7",
|
||||||
"prebuild-gui": "pnpm run tsc",
|
"prebuild-gui": "pnpm run tsc",
|
||||||
"build-windows-gui": "pnpm run prebuild-gui && cd lib && node modules/build windows64 true",
|
"build-windows-gui": "pnpm run prebuild-gui && cd lib && node modules/build windows-x64 true",
|
||||||
"build-ubuntu-gui": "pnpm run prebuild-gui && cd lib && node modules/build ubuntu64 true",
|
"build-linux-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-x64 true",
|
||||||
"build-arm-gui": "pnpm run prebuild-gui && cd lib && node modules/build arm64 true",
|
"build-arm-gui": "pnpm run prebuild-gui && cd lib && node modules/build linux-arm64 true",
|
||||||
"build-macos-gui": "pnpm run prebuild-gui && cd lib && node modules/build macos64 true",
|
"build-macos-gui": "pnpm run prebuild-gui && cd lib && node modules/build macos-x64 true",
|
||||||
|
"build-alpine-gui": "pnpm run prebuild-gui && cd lib && node modules/build alpine-x64 true",
|
||||||
|
"build-android-gui": "pnpm run prebuild-gui && cd lib && node modules/build linuxstatic-armv7 true",
|
||||||
"eslint": "eslint *.js modules",
|
"eslint": "eslint *.js modules",
|
||||||
"eslint-fix": "eslint *.js modules --fix",
|
"eslint-fix": "eslint *.js modules --fix",
|
||||||
"pretest": "pnpm run tsc",
|
"pretest": "pnpm run tsc",
|
||||||
"test": "pnpm run pretest && cd lib && node modules/build windows64 && node modules/build ubuntu64 && node modules/build macos64"
|
"test": "pnpm run pretest && cd lib && node modules/build windows-x64 && node modules/build linuxstatic-x64 && node modules/build macos-x64"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2942
pnpm-lock.yaml
2942
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
6
tsc.ts
6
tsc.ts
|
|
@ -34,6 +34,7 @@ const ignore = [
|
||||||
'./config/updates.json$',
|
'./config/updates.json$',
|
||||||
'./config/cr_token.yml$',
|
'./config/cr_token.yml$',
|
||||||
'./config/funi_token.yml$',
|
'./config/funi_token.yml$',
|
||||||
|
'./config/new_hd_token.yml$',
|
||||||
'./config/hd_token.yml$',
|
'./config/hd_token.yml$',
|
||||||
'./config/hd_sess.yml$',
|
'./config/hd_sess.yml$',
|
||||||
'./config/hd_profile.yml$',
|
'./config/hd_profile.yml$',
|
||||||
|
|
@ -42,7 +43,10 @@ const ignore = [
|
||||||
'./fonts*',
|
'./fonts*',
|
||||||
'./gui/react*',
|
'./gui/react*',
|
||||||
'./dev.js$',
|
'./dev.js$',
|
||||||
'*/node_modules/*'
|
'*/node_modules/*',
|
||||||
|
'./widevine/*',
|
||||||
|
'./videos/*',
|
||||||
|
'./logs/*',
|
||||||
].map(a => a.replace(/\*/g, '[^]*').replace(/\.\//g, escapeRegExp(__dirname) + '/').replace(/\//g, path.sep === '\\' ? '\\\\' : '/')).map(a => new RegExp(a, 'i'));
|
].map(a => a.replace(/\*/g, '[^]*').replace(/\.\//g, escapeRegExp(__dirname) + '/').replace(/\//g, path.sep === '\\' ? '\\\\' : '/')).map(a => new RegExp(a, 'i'));
|
||||||
|
|
||||||
export { ignore };
|
export { ignore };
|
||||||
|
|
|
||||||
0
widevine/.gitkeep
Normal file
0
widevine/.gitkeep
Normal file
Loading…
Reference in a new issue