mp4decrypt -> shaka, jsencrypt -> node-forge, package update

This commit is contained in:
stratuma 2024-06-28 05:07:41 +02:00
parent f5403038e6
commit 1902f58f37
21 changed files with 397 additions and 737 deletions

8
.gitignore vendored
View File

@ -20,11 +20,11 @@ build/
crunchyroll-downloader-output-*/
# FFMPEG
ffmpeg/ffmpeg.exe
ffmpeg/ffprobe.exe
ffmpeg/*
ffmpeg/!.gitkeep
# MP4DECRYPT
mp4decrypt/mp4decrypt.exe
# SHAKA
shaka/shaka.exe
# Keys
keys/client

3
app.config.ts Normal file
View File

@ -0,0 +1,3 @@
export default defineAppConfig({
nuxtIcon: {},
});

View File

@ -244,7 +244,7 @@ export interface CrunchyEpisodeFetch {
data: Array<{
id: string
episode_metadata: {
series_id: string,
series_id: string
season_id: string
}
}>

View File

@ -31,7 +31,7 @@
</template>
<script lang="ts" setup>
import packageJson from '../package.json';
import packageJson from '../package.json'
const isProduction = process.env.NODE_ENV !== 'development'

View File

@ -1,16 +1,14 @@
<template>
<div class="relative flex flex-col items-center justify-center h-full" style="-webkit-app-region: no-drag">
<img src="/logo.png" class="h-24" />
<div class="text-base text-center leading-[18px]">
CrunchyDL
</div>
<div class="text-base text-center leading-[18px]"> CrunchyDL </div>
<div class="text-sm mt-1 text-gray-200"> v{{ packageJson.version }} </div>
<div class="absolute right-0 bottom-0 text-xs text-gray-200"> Made by OpenSTDL </div>
</div>
</template>
<script lang="ts" setup>
import packageJson from '../../package.json';
import packageJson from '../../package.json'
</script>
<style>

View File

@ -1,15 +1,10 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
typescript: {
shim: false
},
ssr: false,
modules: ['@nuxtjs/tailwindcss', 'nuxt-icon', '@nuxtjs/google-fonts'],
googleFonts: {
families: {
'DM+Sans': ['600', '1000'],
'Protest+Riot': true
ssr: false,
modules: ['@nuxtjs/tailwindcss', 'nuxt-icon', '@nuxtjs/google-fonts'],
googleFonts: {
families: {
'DM+Sans': ['600', '1000'],
'Protest+Riot': true
}
}
},
})

View File

@ -8,13 +8,13 @@
"repository": "https://github.com/OpenSTDL/CrunchyDL",
"scripts": {
"dev": "nuxt dev -o",
"build": "nuxt generate",
"build": "nuxt build",
"preview": "nuxt preview",
"postinstall": "nuxt prepare && electron-builder install-app-deps",
"transpile-src": "tsc -p ./src --outDir .output/src",
"dev:electron": "NODE_ENV=development concurrently --kill-others \"nuxt dev\" \"tsc-watch -p ./src --outDir .output/src --onSuccess 'electron ./.output/src/electron/background.js'\"",
"dev:electron:win": "set NODE_ENV=development& concurrently --kill-others \"nuxt dev\" \"tsc-watch -p ./src --outDir .output/src --onSuccess run.electron\"",
"build:electron": "pnpm prettier:fix && pnpm build && pnpm transpile-src && node build.js",
"build:electron": "pnpm prettier:fix && nuxt generate && pnpm transpile-src && node build.js",
"prettier:fix": "pnpm prettier src --write && pnpm prettier components --write && pnpm prettier pages --write && pnpm prettier build.js --write"
},
"devDependencies": {
@ -27,6 +27,7 @@
"@types/express": "^4.17.21",
"@types/fluent-ffmpeg": "^2.1.24",
"@types/node-cron": "^3.0.11",
"@types/node-forge": "^1.3.11",
"concurrently": "^8.2.2",
"dotenv": "^16.4.5",
"electron": "^30.1.2",
@ -34,18 +35,15 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"long": "^5.2.3",
"modclean": "3.0.0-beta.1",
"nuxt": "3.11.2",
"nuxt-icon": "^0.6.10",
"prettier": "^2.8.8",
"protobufjs": "^7.3.2",
"sass": "^1.77.6",
"sass-loader": "^13.3.3",
"tsc-watch": "^6.2.0",
"typescript": "^5.5.2",
"wait-on": "^7.2.0",
"winston": "^3.13.0"
"typescript": "5.4.4",
"wait-on": "^7.2.0"
},
"dependencies": {
"@fastify/cors": "^9.0.1",
@ -58,18 +56,21 @@
"express": "^4.19.2",
"fastify": "^4.28.0",
"fluent-ffmpeg": "^2.1.3",
"jsencrypt": "^3.3.2",
"long": "^5.2.3",
"mpd-parser": "^1.3.0",
"node-cache": "^5.1.2",
"node-cron": "^3.0.3",
"node-forge": "^1.3.1",
"protobufjs": "^7.3.2",
"sequelize": "^6.37.3",
"sqlite3": "5.1.6"
"sqlite3": "5.1.6",
"winston": "^3.13.0"
},
"build": {
"files": [
"!**/pages/*",
"!**/ffmpeg/*",
"!**/mp4decrypt/*",
"!**/shaka/*",
"!**/.git/*",
"!**/.github/*",
"!**/.nuxt/*",
@ -77,7 +78,7 @@
],
"extraResources": [
"./ffmpeg/**",
"./mp4decrypt/**"
"./shaka/**"
]
}
}

View File

@ -234,7 +234,10 @@
</div>
</div>
<div v-if="service === 'crunchyroll'" class="relative flex flex-col select-none">
<div @click="selectHardSub ? (selectHardSub = false) : (selectHardSub = true)" class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
<div
@click="selectHardSub ? (selectHardSub = false) : (selectHardSub = true)"
class="bg-[#5c5b5b] focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer"
>
Hardsub:
{{ selectedHardSub ? `${selectedHardSub.name} (${selectedHardSub.format})` : 'No Hardsub selected' }}
</div>
@ -247,7 +250,7 @@
class="flex flex-row items-center justify-center gap-3 py-2 rounded-xl text-sm"
:class="selectedHardSub && selectedHardSub.locale === l.locale && selectedHardSub.format === 'sub' ? 'bg-[#585858]' : 'hover:bg-[#747474]'"
>
{{ l.name }}<br>(sub)
{{ l.name }}<br />(sub)
</button>
<button
v-for="l in CRselectedShow?.Dubs.map((s) => {
@ -257,7 +260,7 @@
class="flex flex-row items-center justify-center gap-3 py-2 rounded-xl text-sm"
:class="selectedHardSub && selectedHardSub.locale === l.locale && selectedHardSub.format === 'dub' ? 'bg-[#585858]' : 'hover:bg-[#747474]'"
>
{{ l.name }}<br>(dub)
{{ l.name }}<br />(dub)
</button>
</div>
</div>
@ -429,7 +432,7 @@ const selectSub = ref<boolean>(false)
const selectedSubs = ref<Array<{ name: string | undefined; locale: string }>>([])
const selectHardSub = ref<boolean>(false)
const selectedHardSub = ref<{ name: string | undefined; locale: string, format: string }>()
const selectedHardSub = ref<{ name: string | undefined; locale: string; format: string }>()
const tab = ref<number>(1)
const search = ref<string>('')
@ -779,7 +782,7 @@ const switchToSeason = async () => {
if (url.value && url.value.includes('crunchyroll') && url.value.includes('/watch/') && !CRselectedShow.value) {
var episodeID: string | string[] = url.value.split('/')
episodeID = episodeID[episodeID.length-2]
episodeID = episodeID[episodeID.length - 2]
const seriesID = await getCREpisodeSeriesID(episodeID)
if (!seriesID) {
alert('Episode not found')
@ -798,7 +801,7 @@ const switchToSeason = async () => {
isFetchingSeasons.value--
return
}
selectedSeason.value = seasons.value.find(s => s.id === seriesID.season) ?? seasons.value[0]
selectedSeason.value = seasons.value.find((s) => s.id === seriesID.season) ?? seasons.value[0]
episodes.value = await listEpisodeCrunchy(selectedSeason.value.id, CRselectedShow.value.Geo)
if (episodes.value) {
selectedStartEpisode.value = episodes.value[0]

View File

@ -127,7 +127,7 @@
</div>
<div class="relative flex flex-row gap-2 h-full items-end">
<div class="text-xs">{{ p.quality }}p</div>
<div v-if="p.qualityaudio" class="text-xs">{{ audioQualities[p.qualityaudio-1] ?? '44.10 kHz' }}</div>
<div v-if="p.qualityaudio" class="text-xs">{{ audioQualities[p.qualityaudio - 1] ?? '44.10 kHz' }}</div>
<div class="text-xs uppercase">{{ p.format }}</div>
<div class="text-xs">Dubs: {{ p.dub.map((t) => t.name).join(', ') }}</div>
<div class="text-xs">Subs: {{ p.sub.length !== 0 ? p.sub.map((t) => t.name).join(', ') : '-' }}</div>
@ -166,7 +166,7 @@ const playlist = ref<
media: CrunchyEpisode | ADNEpisode
dub: Array<{ locale: string; name: string }>
sub: Array<{ locale: string; name: string }>
hardsub: { name: string | undefined; locale: string, format: string }
hardsub: { name: string | undefined; locale: string; format: string }
dir: string
installDir: string
partsleft: number
@ -194,7 +194,7 @@ const getPlaylist = async () => {
media: CrunchyEpisode | ADNEpisode
dub: Array<{ locale: string; name: string }>
sub: Array<{ locale: string; name: string }>
hardsub: { name: string | undefined; locale: string, format: string }
hardsub: { name: string | undefined; locale: string; format: string }
dir: string
installDir: string
partsleft: number

File diff suppressed because it is too large Load Diff

View File

@ -46,7 +46,7 @@ interface PlaylistAttributes {
media: CrunchyEpisode | ADNEpisode
dub: { name: string | undefined; locale: string }[]
sub: { name: string | undefined; locale: string }[]
hardsub: { name: string | undefined; locale: string, format: string }
hardsub: { name: string | undefined; locale: string; format: string }
quality: 1080 | 720 | 480 | 360 | 240
qualityaudio: 1 | 2 | 3 | undefined
dir: string
@ -60,7 +60,7 @@ interface PlaylistCreateAttributes {
media: CrunchyEpisode | ADNEpisode
dub: { name: string | undefined; locale: string }[]
sub: { name: string | undefined; locale: string }[]
hardsub: { name: string | undefined; locale: string, format: string } | undefined
hardsub: { name: string | undefined; locale: string; format: string } | undefined
dir: string
quality: 1080 | 720 | 480 | 360 | 240
qualityaudio: 1 | 2 | 3 | undefined

View File

@ -1,11 +1,10 @@
import JSEncrypt from 'jsencrypt'
import CryptoJS from 'crypto-js'
import { server } from '../../api'
import { ADNLink, ADNPlayerConfig } from '../../types/adn'
import { messageBox } from '../../../electron/background'
import { useFetch } from '../useFetch'
import { loggedInCheck } from '../service/service.service'
import { parse as mpdParse, parse } from 'mpd-parser'
import * as forge from 'node-forge'
export async function adnLogin(user: string, passw: string) {
const cachedData:
@ -178,12 +177,9 @@ async function getPlayerEncryptedToken(id: number, geo: 'de' | 'fr') {
if (!token) return
var key = new JSEncrypt()
var random = randomHexaString(16)
const publicKeyPem = `-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssgnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6KhS+IFEqwvZqgbBpKuwIDAQAB-----END PUBLIC KEY-----`
key.setPublicKey(
'-----BEGIN PUBLIC KEY-----MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbQrCJBRmaXM4gJidDmcpWDssgnumHinCLHAgS4buMtdH7dEGGEUfBofLzoEdt1jqcrCDT6YNhM0aFCqbLOPFtx9cg/X2G/G5bPVu8cuFM0L+ehp8s6izK1kjx3OOPH/kWzvstM5tkqgJkNyNEvHdeJl6KhS+IFEqwvZqgbBpKuwIDAQAB-----END PUBLIC KEY-----'
)
var random = CryptoJS.lib.WordArray.random(16).toString(CryptoJS.enc.Hex)
const data = {
k: random,
@ -192,11 +188,19 @@ async function getPlayerEncryptedToken(id: number, geo: 'de' | 'fr') {
const finisheddata = JSON.stringify(data)
const encryptedData = key.encrypt(finisheddata) || ''
const encryptedData = encryptWithPublicKey(publicKeyPem, finisheddata)
return { data: encryptedData, random: random }
}
function encryptWithPublicKey(publicKeyPem: string, data: string) {
const publicKey = forge.pki.publicKeyFromPem(publicKeyPem)
const encryptedData = publicKey.encrypt(data, 'RSA-OAEP', {
md: forge.md.sha256.create()
})
return forge.util.encode64(encryptedData)
}
export async function adnGetPlaylist(animeid: number, geo: 'de' | 'fr') {
const token = await getPlayerEncryptedToken(animeid, geo)

View File

@ -98,7 +98,7 @@ export async function addPlaylistController(
episodes: CrunchyEpisodes
dubs: { name: string | undefined; locale: string }[]
subs: { name: string | undefined; locale: string }[]
hardsub: { name: string | undefined; locale: string, format: string } | undefined
hardsub: { name: string | undefined; locale: string; format: string } | undefined
dir: string
quality: 1080 | 720 | 480 | 360 | 240
qualityaudio: 1 | 2 | 3 | undefined

View File

@ -15,9 +15,9 @@ import { ADNEpisode } from '../../types/adn'
import { messageBox } from '../../../electron/background'
import { getFFMPEGPath } from '../../services/ffmpeg'
import { getDRMKeys, Uint8ArrayToBase64 } from '../../services/decryption'
import { getMP4DecryptPath } from '../../services/mp4decrypt'
import { getShakaPath } from '../../services/shaka'
const ffmpegP = getFFMPEGPath()
const mp4e = getMP4DecryptPath()
const shaka = getShakaPath()
import util from 'util'
import settings from 'electron-settings'
import { server } from '../../api'
@ -211,7 +211,7 @@ export async function addEpisodeToPlaylist(
e: CrunchyEpisode,
s: { name: string | undefined; locale: string }[],
d: { name: string | undefined; locale: string }[],
hardsub: { name: string | undefined; locale: string, format: string } | undefined,
hardsub: { name: string | undefined; locale: string; format: string } | undefined,
dir: string,
status:
| 'waiting'
@ -471,7 +471,7 @@ export async function downloadCrunchyrollPlaylist(
e: string,
dubs: Array<string>,
subs: Array<string>,
hardsub: { name: string | undefined; locale: string; format: string; },
hardsub: { name: string | undefined; locale: string; format: string },
episodeID: string,
downloadID: number,
name: string,
@ -792,7 +792,7 @@ export async function downloadCrunchyrollPlaylist(
let p: { filename: string; url: string }[] = []
if (playlist.mediaGroups.AUDIO.audio.main.playlists[playlistindex].contentProtection) {
if (!playlist.mediaGroups.AUDIO.audio.main.playlists[playlistindex].contentProtection['com.widevine.alpha'].pssh) {
if (!playlist.mediaGroups.AUDIO.audio.main.playlists![playlistindex]!.contentProtection!['com.widevine.alpha']!.pssh) {
console.log('No PSSH found, exiting.')
messageBox(
'error',
@ -811,7 +811,7 @@ export async function downloadCrunchyrollPlaylist(
})
return
}
pssh = Uint8ArrayToBase64(playlist.mediaGroups.AUDIO.audio.main.playlists[playlistindex].contentProtection['com.widevine.alpha'].pssh)
pssh = Uint8ArrayToBase64(playlist.mediaGroups.AUDIO.audio.main.playlists![playlistindex]!.contentProtection!['com.widevine.alpha'].pssh!)
keys = await getDRMKeys(pssh, assetId[1], list.account_id)
@ -946,9 +946,9 @@ export async function downloadCrunchyrollPlaylist(
var downloadGEO
if (hardsub && hardsub.locale) {
var hardsubURL: string | undefined;
var hardsubURL: string | undefined
var hardsubGEO: string | undefined;;
var hardsubGEO: string | undefined
if (hardsub.format === 'dub') {
const found = play.data.versions.find((h) => h.audio_locale === hardsub.locale)
@ -989,7 +989,14 @@ export async function downloadCrunchyrollPlaylist(
downloadURL = play.data.url
downloadGEO = play.data.geo
console.log('Hardsub Playlist not found')
messageBox('warning', ['Cancel'], 2, 'Hardsub Playlist not found', 'Hardsub Playlist not found', `${hardsub.locale} Hardsub Playlist not found, downloading japanese playlist instead.`)
messageBox(
'warning',
['Cancel'],
2,
'Hardsub Playlist not found',
'Hardsub Playlist not found',
`${hardsub.locale} Hardsub Playlist not found, downloading japanese playlist instead.`
)
server.logger.log({
level: 'error',
message: `${hardsub.locale} Hardsub Playlist not found, downloading japanese playlist instead.`,
@ -1296,11 +1303,8 @@ async function mergeParts(parts: { filename: string; url: string }[], downloadID
await updatePlaylistByID(downloadID, 'decrypting video')
console.log('Video Decryption started')
const inputFilePath = `${tmp}/temp-main.m4s`
const outputFilePath = `${tmp}/main.m4s`
const keyArgument = `--show-progress --key ${drmkeys[1].kid}:${drmkeys[1].key}`
const command = `${mp4e} ${keyArgument} "${inputFilePath}" "${outputFilePath}"`
const command = `${shaka} input="${tmp}/temp-main.m4s",stream=video,output="${tmp}/main.m4s" --enable_raw_key_decryption --keys key_id=${drmkeys[1].kid}:key=${drmkeys[1].key}`
await exec(command)
console.log('Video Decryption finished')
@ -1397,15 +1401,13 @@ async function mergeVideoFile(
if (format === 'mp4') {
options.push('-c:s mov_text')
}
for (const [index, a] of audios.entries()) {
output.addInput(a)
options.push(`-map ${ffindex}:a:0`)
options.push(
`-metadata:s:a:${index} language=${
locales.find((l) => l.locale === getFilename(a, '.aac', '/'))
? locales.find((l) => l.locale === getFilename(a, '.aac', '/'))?.iso
: getFilename(a, '.aac', '/')
locales.find((l) => l.locale === getFilename(a, '.aac', '/')) ? locales.find((l) => l.locale === getFilename(a, '.aac', '/'))?.iso : getFilename(a, '.aac', '/')
}`
)
options.push(
@ -1415,7 +1417,7 @@ async function mergeVideoFile(
: getFilename(a, '.aac', '/')
}`
)
ffindex++
}

View File

@ -4,9 +4,9 @@ import { checkFileExistence, createFolder, deleteFolder } from './folder'
import { concatenateTSFiles } from './concatenate'
import Ffmpeg from 'fluent-ffmpeg'
import { getFFMPEGPath } from './ffmpeg'
import { getMP4DecryptPath } from '../services/mp4decrypt'
import { getShakaPath } from './shaka'
const ffmpegP = getFFMPEGPath()
const mp4e = getMP4DecryptPath()
const shaka = getShakaPath()
import util from 'util'
import { server } from '../api'
const exec = util.promisify(require('child_process').exec)
@ -161,11 +161,8 @@ async function mergePartsAudio(
dn.status = 'decrypting'
}
console.log(`Audio Decryption started`)
const inputFilePath = `${tmp}/temp-main.m4s`
const outputFilePath = `${tmp}/main.m4s`
const keyArgument = `--show-progress --key ${drmkeys[1].kid}:${drmkeys[1].key}`
const command = `${mp4e} ${keyArgument} "${inputFilePath}" "${outputFilePath}"`
const command = `${shaka} input="${tmp}/temp-main.m4s",stream=audio,output="${tmp}/main.m4s" --enable_raw_key_decryption --keys key_id=${drmkeys[1].kid}:key=${drmkeys[1].key}`
await exec(command)
concatenatedFile = `${tmp}/main.m4s`

View File

@ -105,8 +105,8 @@ export function getFilename(path: string, ext: string, delimiter: string) {
const segments = path.split(delimiter)
if (segments.length == 0) {
return "unkown"
return 'unkown'
}
return segments[segments.length - 1].split(ext)[0]
}
}

View File

@ -1,21 +0,0 @@
import { app } from 'electron'
import path from 'path'
const isDev = process.env.NODE_ENV === 'development'
const appPath = app.getAppPath()
const resourcesPath = path.dirname(appPath)
const decryptPath = path.join(resourcesPath, 'mp4decrypt')
if (isDev) {
require('dotenv').config()
}
export function getMP4DecryptPath() {
if (isDev) {
const mp4Decrypt = process.env.MP4DECRYPT_PATH
return mp4Decrypt
} else {
const mp4Decrypt = `"${path.join(decryptPath, 'mp4decrypt.exe')}"`
return mp4Decrypt
}
}

21
src/api/services/shaka.ts Normal file
View File

@ -0,0 +1,21 @@
import { app } from 'electron'
import path from 'path'
const isDev = process.env.NODE_ENV === 'development'
const appPath = app.getAppPath()
const resourcesPath = path.dirname(appPath)
const shakaFolderPath = path.join(resourcesPath, 'shaka')
if (isDev) {
require('dotenv').config()
}
export function getShakaPath() {
if (isDev) {
const shaka = process.env.SHAKA_PATH
return shaka
} else {
const shaka = `"${path.join(shakaFolderPath, 'shaka.exe')}"`
return shaka
}
}

View File

@ -17,7 +17,6 @@ export async function downloadCRSub(
qual: 1080 | 720 | 480 | 360 | 240
) {
try {
var resamplerActive = await settings.get('subtitleResamplerActive')
if (resamplerActive === undefined || resamplerActive === null) {
@ -59,8 +58,8 @@ export async function downloadCRSub(
await finished(readableStream.pipe(stream))
console.log(`Sub ${sub.language}.${sub.format} downloaded`)
return path
return path
}
var parsedASS = parse(await response.text())

View File

@ -1,23 +1,15 @@
// This is the dynamic renderer script for Electron.
// You can implement your custom renderer process configuration etc. here!
// --------------------------------------------
import * as path from 'path'
import { BrowserWindow } from 'electron'
import express, { static as serveStatic } from 'express'
// Internals
// =========
const isProduction = process.env.NODE_ENV !== 'development'
// Dynamic Renderer
// ================
export default async function (mainWindow: BrowserWindow) {
if (!isProduction) return mainWindow.loadURL('http://localhost:3000/')
const app = express()
app.use('/', serveStatic(path.join(__dirname, '../../public')))
const listener = app.listen(8079, 'localhost', () => {
const port = (listener.address() as any).port
console.log('Dynamic-Renderer Listening on', port)
mainWindow.loadURL(`http://localhost:${port}`)
})
}