more error handeling for widevine keys
This commit is contained in:
parent
40126d5b82
commit
f86d8f7aa1
@ -1,13 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="flex flex-col gap-3 mt-3 font-dm" style="-webkit-app-region: no-drag">
|
||||||
|
<div class="flex flex-col items-center p-3 bg-[#11111189] rounded-xl select-none">
|
||||||
|
<div class="text-sm mb-2">Stream Endpoint</div>
|
||||||
|
<select v-model="selectedEndpoint" @change="selectEndpoint()" class="bg-[#5c5b5b] w-full focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer">
|
||||||
|
<option :value="1">Switch</option>
|
||||||
|
<option :value="2">PS4 (DRM PROTECTED)</option>
|
||||||
|
<option :value="3">PS5 (DRM PROTECTED)</option>
|
||||||
|
<option :value="4">XBOX One (DRM PROTECTED)</option>
|
||||||
|
<option :value="5">Firefox (DRM PROTECTED)</option>
|
||||||
|
<option :value="6">Edge (DRM PROTECTED)</option>
|
||||||
|
<option :value="7">Safari (DRM PROTECTED)</option>
|
||||||
|
<option :value="8">Chrome (DRM PROTECTED)</option>
|
||||||
|
<option :value="9">Web Fallback (DRM PROTECTED)</option>
|
||||||
|
<option :value="10">Iphone (DRM PROTECTED)</option>
|
||||||
|
<option :value="11">Ipad (DRM PROTECTED)</option>
|
||||||
|
<option :value="12">Android (DRM PROTECTED)</option>
|
||||||
|
<option :value="13">Samsung TV (DRM PROTECTED) (Fastest)</option>
|
||||||
|
</select>
|
||||||
|
<div class="text-xs mt-2">
|
||||||
|
Fallback to non-drm stream if no widevine key provided
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
const selectedEndpoint = ref<number>()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
;(window as any).myAPI.getEndpoint().then((result: any) => {
|
||||||
|
selectedEndpoint.value = result
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectEndpoint = () => {
|
||||||
|
if (process.client) {
|
||||||
|
;(window as any).myAPI.selectEndpoint(selectedEndpoint.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
select {
|
||||||
|
background: url("data:image/svg+xml,<svg height='10px' width='10px' viewBox='0 0 16 16' fill='%23000000' xmlns='http://www.w3.org/2000/svg'><path d='M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z'/></svg>")
|
||||||
|
no-repeat;
|
||||||
|
background-position: calc(100% - 0.75rem) center !important;
|
||||||
|
background-color: #5c5b5b;
|
||||||
|
-moz-appearance: none !important;
|
||||||
|
-webkit-appearance: none !important;
|
||||||
|
appearance: none !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col mt-3 font-dm" style="-webkit-app-region: no-drag">
|
<div class="flex flex-col mt-3 font-dm" style="-webkit-app-region: no-drag">
|
||||||
<div class="flex flex-col items-center h-40 p-3 bg-[#11111189] rounded-xl select-none">
|
<div class="flex flex-col items-center p-3 bg-[#11111189] rounded-xl select-none">
|
||||||
<div class="text-sm mb-2"> Account Management </div>
|
<div class="text-sm mb-2"> Account Management </div>
|
||||||
<div v-for="account in accounts" class="flex flex-row items-center h-12 p-3 w-full bg-[#4b4b4b89] rounded-xl">
|
<div v-for="account in accounts" class="flex flex-row items-center h-12 p-3 w-full bg-[#4b4b4b89] rounded-xl">
|
||||||
<Icon v-if="account.service === 'CR'" name="simple-icons:crunchyroll" class="h-6 w-6 text-white" />
|
<Icon v-if="account.service === 'CR'" name="simple-icons:crunchyroll" class="h-6 w-6 text-white" />
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-center p-3 bg-[#11111189] rounded-xl select-none">
|
<!-- <div class="flex flex-col items-center p-3 bg-[#11111189] rounded-xl select-none">
|
||||||
<div class="text-sm mb-2">L1 Keys</div>
|
<div class="text-sm mb-2">L1 Keys</div>
|
||||||
<input
|
<input
|
||||||
@click="getFilePathL1Blob()"
|
@click="getFilePathL1Blob()"
|
||||||
@ -41,7 +41,7 @@
|
|||||||
class="bg-[#5c5b5b] w-full focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer mt-2"
|
class="bg-[#5c5b5b] w-full focus:outline-none px-3 py-2 rounded-xl text-sm text-center cursor-pointer mt-2"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -68,7 +68,6 @@
|
|||||||
"!**/pages/*",
|
"!**/pages/*",
|
||||||
"!**/ffmpeg/*",
|
"!**/ffmpeg/*",
|
||||||
"!**/mp4decrypt/*",
|
"!**/mp4decrypt/*",
|
||||||
"!**/keys/*",
|
|
||||||
"!**/.git/*",
|
"!**/.git/*",
|
||||||
"!**/.github/*",
|
"!**/.github/*",
|
||||||
"!**/.nuxt/*",
|
"!**/.nuxt/*",
|
||||||
@ -76,8 +75,7 @@
|
|||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
"./ffmpeg/**",
|
"./ffmpeg/**",
|
||||||
"./mp4decrypt/**",
|
"./mp4decrypt/**"
|
||||||
"./keys/**"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,10 @@ import { VideoPlaylist } from '../../types/crunchyroll'
|
|||||||
import { useFetch } from '../useFetch'
|
import { useFetch } from '../useFetch'
|
||||||
import { parse as mpdParse } from 'mpd-parser'
|
import { parse as mpdParse } from 'mpd-parser'
|
||||||
import { loggedInCheck } from '../service/service.service'
|
import { loggedInCheck } from '../service/service.service'
|
||||||
|
import settings from 'electron-settings'
|
||||||
|
|
||||||
// Disable when Crunchyroll turns off switch endpoint
|
// Disable when Crunchyroll turns off switch endpoint
|
||||||
const enableDRMBypass = false
|
const enableDRMBypass = true
|
||||||
|
|
||||||
// Crunchyroll Error message list
|
// Crunchyroll Error message list
|
||||||
const crErrors = [
|
const crErrors = [
|
||||||
@ -110,6 +111,84 @@ async function crunchyLoginFetch(user: string, passw: string) {
|
|||||||
|
|
||||||
// Crunchyroll Playlist Fetch
|
// Crunchyroll Playlist Fetch
|
||||||
export async function crunchyGetPlaylist(q: string) {
|
export async function crunchyGetPlaylist(q: string) {
|
||||||
|
|
||||||
|
var endpoint = await settings.get('CREndpoint');
|
||||||
|
const drmL3blob = await settings.get('l3blob');
|
||||||
|
const drmL3key = await settings.get('l3key');
|
||||||
|
|
||||||
|
if (!drmL3blob || !drmL3key) {
|
||||||
|
await settings.set('CREndpoint', 1);
|
||||||
|
endpoint = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoints: { id: number, name: string, url: string }[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Switch',
|
||||||
|
url: `/console/switch/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'PS4',
|
||||||
|
url: `/console/ps4/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'PS5',
|
||||||
|
url: `/console/ps5/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'XBOX One',
|
||||||
|
url: `/console/xbox_one/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: 'Firefox',
|
||||||
|
url: `/web/firefox/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
name: 'Edge',
|
||||||
|
url: `/web/edge/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
name: 'Safari',
|
||||||
|
url: `/web/safari/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
name: 'Chrome',
|
||||||
|
url: `/web/chrome/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
name: 'Web Fallback',
|
||||||
|
url: `/web/fallback/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
name: 'Iphone',
|
||||||
|
url: `/ios/iphone/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
name: 'Ipad',
|
||||||
|
url: `/ios/ipad/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12,
|
||||||
|
name: 'Android',
|
||||||
|
url: `/android/phone/play`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 13,
|
||||||
|
name: 'Samsung TV',
|
||||||
|
url: `/tv/samsung/play`
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const account = await loggedInCheck('CR')
|
const account = await loggedInCheck('CR')
|
||||||
|
|
||||||
if (!account) return
|
if (!account) return
|
||||||
@ -125,9 +204,7 @@ export async function crunchyGetPlaylist(q: string) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
enableDRMBypass
|
`https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}${endpoints.find(e=> e.id === endpoint) ? endpoints.find(e=> e.id === endpoint)?.url : '/console/switch/play'}`,
|
||||||
? `https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}/tv/samsung/play`
|
|
||||||
: `https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}/console/switch/play`,
|
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: headers
|
headers: headers
|
||||||
@ -143,7 +220,11 @@ export async function crunchyGetPlaylist(q: string) {
|
|||||||
|
|
||||||
return { data: data, account_id: login.account_id }
|
return { data: data, account_id: login.account_id }
|
||||||
} else {
|
} else {
|
||||||
throw new Error(await response.text())
|
|
||||||
|
const error = await response.text()
|
||||||
|
|
||||||
|
messageBox('error', ['Cancel'], 2, 'Failed to get MPD Playlist', 'Failed to get MPD Playlist', error)
|
||||||
|
throw new Error(error)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new Error(e as string)
|
throw new Error(e as string)
|
||||||
|
@ -19,6 +19,7 @@ import { getMP4DecryptPath } from '../../services/mp4decrypt'
|
|||||||
const ffmpegP = getFFMPEGPath()
|
const ffmpegP = getFFMPEGPath()
|
||||||
const mp4e = getMP4DecryptPath()
|
const mp4e = getMP4DecryptPath()
|
||||||
import util from 'util'
|
import util from 'util'
|
||||||
|
import settings from 'electron-settings'
|
||||||
const exec = util.promisify(require('child_process').exec)
|
const exec = util.promisify(require('child_process').exec)
|
||||||
|
|
||||||
// Get All Accounts
|
// Get All Accounts
|
||||||
@ -366,6 +367,9 @@ export async function downloadCrunchyrollPlaylist(
|
|||||||
|
|
||||||
const seasonFolder = await createFolderName(`${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season}`, downloadPath)
|
const seasonFolder = await createFolderName(`${name.replace(/[/\\?%*:|"<>]/g, '')} Season ${season}`, downloadPath)
|
||||||
|
|
||||||
|
const drmL3blob = await settings.get('l3blob')
|
||||||
|
const drmL3key = await settings.get('l3key')
|
||||||
|
|
||||||
const dubDownloadList: Array<{
|
const dubDownloadList: Array<{
|
||||||
audio_locale: string
|
audio_locale: string
|
||||||
guid: string
|
guid: string
|
||||||
@ -509,6 +513,22 @@ export async function downloadCrunchyrollPlaylist(
|
|||||||
keys = await getDRMKeys(pssh, assetId[1], list.account_id)
|
keys = await getDRMKeys(pssh, assetId[1], list.account_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(playlist.mediaGroups.AUDIO.audio.main.playlists[0].contentProtection && !drmL3blob && !drmL3key) ||
|
||||||
|
(playlist.mediaGroups.AUDIO.audio.main.playlists[0].contentProtection && !drmL3blob) ||
|
||||||
|
(playlist.mediaGroups.AUDIO.audio.main.playlists[0].contentProtection && !drmL3key)
|
||||||
|
) {
|
||||||
|
await updatePlaylistByID(downloadID, 'failed')
|
||||||
|
messageBox(
|
||||||
|
'error',
|
||||||
|
['Cancel'],
|
||||||
|
2,
|
||||||
|
'Audio Widevine encrypted but no key provided',
|
||||||
|
'Audio Widevine encrypted but no key provided',
|
||||||
|
'To download Widevine encrypted videos add the L3 Widevine keys in Settings > Widewine > L3 Keys'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
p.push({
|
p.push({
|
||||||
filename: (playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].map.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
filename: (playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].map.uri.match(/([^\/]+)\?/) as RegExpMatchArray)[1],
|
||||||
url: playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].map.resolvedUri
|
url: playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].map.resolvedUri
|
||||||
@ -604,6 +624,23 @@ export async function downloadCrunchyrollPlaylist(
|
|||||||
keys = await getDRMKeys(pssh, assetId[1], play.account_id)
|
keys = await getDRMKeys(pssh, assetId[1], play.account_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(hq.contentProtection && !drmL3blob && !drmL3key) ||
|
||||||
|
(hq.contentProtection && !drmL3blob) ||
|
||||||
|
(hq.contentProtection && !drmL3key)
|
||||||
|
) {
|
||||||
|
await updatePlaylistByID(downloadID, 'failed')
|
||||||
|
messageBox(
|
||||||
|
'error',
|
||||||
|
['Cancel'],
|
||||||
|
2,
|
||||||
|
'Audio Widevine encrypted but no key provided',
|
||||||
|
'Audio Widevine encrypted but no key provided',
|
||||||
|
'To download Widevine encrypted videos add the L3 Widevine keys in Settings > Widewine > L3 Keys'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var p: { filename: string; url: string }[] = []
|
var p: { filename: string; url: string }[] = []
|
||||||
|
|
||||||
p.push({
|
p.push({
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import { Session } from '../modules/license'
|
import { Session } from '../modules/license'
|
||||||
import { readFileSync } from 'fs'
|
import { readFileSync } from 'fs'
|
||||||
import { getWVKPath } from './widevine'
|
import { getWVKPath } from './widevine'
|
||||||
const keys = getWVKPath()
|
|
||||||
|
|
||||||
export async function getDRMKeys(pssh: string, assetID: string, userID: string) {
|
export async function getDRMKeys(pssh: string, assetID: string, userID: string) {
|
||||||
const auth = await getWVKey(assetID, userID)
|
const auth = await getWVKey(assetID, userID)
|
||||||
const depssh = Buffer.from(pssh, 'base64')
|
const depssh = Buffer.from(pssh, 'base64')
|
||||||
|
|
||||||
|
const keys = await getWVKPath()
|
||||||
|
|
||||||
if (!keys) return
|
if (!keys) return
|
||||||
|
|
||||||
if (!keys.key) return
|
if (!keys.key) return
|
||||||
|
@ -1,23 +1,14 @@
|
|||||||
import { app } from 'electron'
|
import { app } from 'electron'
|
||||||
|
import settings from 'electron-settings'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
const isDev = process.env.NODE_ENV === 'development'
|
|
||||||
const appPath = app.getAppPath()
|
export async function getWVKPath() {
|
||||||
const resourcesPath = path.dirname(appPath)
|
const drmL3blob = await settings.get('l3blob') as string
|
||||||
const keyPath = path.join(resourcesPath, 'keys')
|
const drmL3key = await settings.get('l3key') as string
|
||||||
if (isDev) {
|
|
||||||
require('dotenv').config()
|
if (!drmL3blob || !drmL3key) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWVKPath() {
|
return { key: drmL3key, client: drmL3blob }
|
||||||
if (isDev) {
|
|
||||||
const key = process.env.KEY_KEY
|
|
||||||
const client = process.env.KEY_CLIENT
|
|
||||||
|
|
||||||
return { key: key, client: client }
|
|
||||||
} else {
|
|
||||||
const key = path.join(keyPath, 'key')
|
|
||||||
const client = path.join(keyPath, 'client')
|
|
||||||
|
|
||||||
return { key: key, client: client }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -167,6 +167,24 @@ ipcMain.handle('dialog:defaultDirectory', async () => {
|
|||||||
return savedPath
|
return savedPath
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('dialog:selectEndpoint', async (events, nr: number) => {
|
||||||
|
await settings.set('CREndpoint', nr)
|
||||||
|
|
||||||
|
return nr
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('dialog:getEndpoint', async (events, nr: number) => {
|
||||||
|
const endpointNr = await settings.get('CREndpoint')
|
||||||
|
|
||||||
|
if (!endpointNr) {
|
||||||
|
await settings.set('CREndpoint', 1)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpointNr
|
||||||
|
})
|
||||||
|
|
||||||
ipcMain.handle('dialog:defaultFile', async (events, type: string) => {
|
ipcMain.handle('dialog:defaultFile', async (events, type: string) => {
|
||||||
|
|
||||||
if (!type) return
|
if (!type) return
|
||||||
|
@ -3,6 +3,8 @@ import { contextBridge, ipcRenderer } from 'electron'
|
|||||||
contextBridge.exposeInMainWorld('myAPI', {
|
contextBridge.exposeInMainWorld('myAPI', {
|
||||||
selectFolder: () => ipcRenderer.invoke('dialog:openDirectory'),
|
selectFolder: () => ipcRenderer.invoke('dialog:openDirectory'),
|
||||||
selectFile: (type: string) => ipcRenderer.invoke('dialog:openFile', type),
|
selectFile: (type: string) => ipcRenderer.invoke('dialog:openFile', type),
|
||||||
|
selectEndpoint: (nr: number) => ipcRenderer.invoke('dialog:selectEndpoint', nr),
|
||||||
|
getEndpoint: () => ipcRenderer.invoke('dialog:getEndpoint'),
|
||||||
getFolder: () => ipcRenderer.invoke('dialog:defaultDirectory'),
|
getFolder: () => ipcRenderer.invoke('dialog:defaultDirectory'),
|
||||||
getFile: (type: string) => ipcRenderer.invoke('dialog:defaultFile', type),
|
getFile: (type: string) => ipcRenderer.invoke('dialog:defaultFile', type),
|
||||||
openWindow: (opt: { title: string; url: string; width: number; height: number; backgroundColor: string }) => ipcRenderer.invoke('window:openNewWindow', opt),
|
openWindow: (opt: { title: string; url: string; width: number; height: number; backgroundColor: string }) => ipcRenderer.invoke('window:openNewWindow', opt),
|
||||||
|
Reference in New Issue
Block a user