Account Management
diff --git a/components/Settings/Widevine.vue b/components/Settings/Widevine.vue
index bbe6302..7246d2b 100644
--- a/components/Settings/Widevine.vue
+++ b/components/Settings/Widevine.vue
@@ -21,7 +21,7 @@
readonly
/>
-
+
diff --git a/keys/.gitkeep b/keys/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/package.json b/package.json
index 69db1ba..6fa14d2 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,6 @@
"!**/pages/*",
"!**/ffmpeg/*",
"!**/mp4decrypt/*",
- "!**/keys/*",
"!**/.git/*",
"!**/.github/*",
"!**/.nuxt/*",
@@ -76,8 +75,7 @@
],
"extraResources": [
"./ffmpeg/**",
- "./mp4decrypt/**",
- "./keys/**"
+ "./mp4decrypt/**"
]
}
}
diff --git a/src/api/routes/crunchyroll/crunchyroll.service.ts b/src/api/routes/crunchyroll/crunchyroll.service.ts
index f47d796..5a21210 100644
--- a/src/api/routes/crunchyroll/crunchyroll.service.ts
+++ b/src/api/routes/crunchyroll/crunchyroll.service.ts
@@ -4,9 +4,10 @@ import { VideoPlaylist } from '../../types/crunchyroll'
import { useFetch } from '../useFetch'
import { parse as mpdParse } from 'mpd-parser'
import { loggedInCheck } from '../service/service.service'
+import settings from 'electron-settings'
// Disable when Crunchyroll turns off switch endpoint
-const enableDRMBypass = false
+const enableDRMBypass = true
// Crunchyroll Error message list
const crErrors = [
@@ -110,6 +111,84 @@ async function crunchyLoginFetch(user: string, passw: string) {
// Crunchyroll Playlist Fetch
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')
if (!account) return
@@ -125,9 +204,7 @@ export async function crunchyGetPlaylist(q: string) {
try {
const response = await fetch(
- enableDRMBypass
- ? `https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}/tv/samsung/play`
- : `https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}/console/switch/play`,
+ `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'}`,
{
method: 'GET',
headers: headers
@@ -143,7 +220,11 @@ export async function crunchyGetPlaylist(q: string) {
return { data: data, account_id: login.account_id }
} 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) {
throw new Error(e as string)
diff --git a/src/api/routes/service/service.service.ts b/src/api/routes/service/service.service.ts
index 426e009..812820e 100644
--- a/src/api/routes/service/service.service.ts
+++ b/src/api/routes/service/service.service.ts
@@ -19,6 +19,7 @@ import { getMP4DecryptPath } from '../../services/mp4decrypt'
const ffmpegP = getFFMPEGPath()
const mp4e = getMP4DecryptPath()
import util from 'util'
+import settings from 'electron-settings'
const exec = util.promisify(require('child_process').exec)
// Get All Accounts
@@ -366,6 +367,9 @@ export async function downloadCrunchyrollPlaylist(
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<{
audio_locale: string
guid: string
@@ -509,6 +513,22 @@ export async function downloadCrunchyrollPlaylist(
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({
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
@@ -604,6 +624,23 @@ export async function downloadCrunchyrollPlaylist(
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 }[] = []
p.push({
diff --git a/src/api/services/decryption.ts b/src/api/services/decryption.ts
index 540d24c..78933a0 100644
--- a/src/api/services/decryption.ts
+++ b/src/api/services/decryption.ts
@@ -1,12 +1,13 @@
import { Session } from '../modules/license'
import { readFileSync } from 'fs'
import { getWVKPath } from './widevine'
-const keys = getWVKPath()
export async function getDRMKeys(pssh: string, assetID: string, userID: string) {
const auth = await getWVKey(assetID, userID)
const depssh = Buffer.from(pssh, 'base64')
+ const keys = await getWVKPath()
+
if (!keys) return
if (!keys.key) return
diff --git a/src/api/services/widevine.ts b/src/api/services/widevine.ts
index 762e1c4..489f913 100644
--- a/src/api/services/widevine.ts
+++ b/src/api/services/widevine.ts
@@ -1,23 +1,14 @@
import { app } from 'electron'
+import settings from 'electron-settings'
import path from 'path'
-const isDev = process.env.NODE_ENV === 'development'
-const appPath = app.getAppPath()
-const resourcesPath = path.dirname(appPath)
-const keyPath = path.join(resourcesPath, 'keys')
-if (isDev) {
- require('dotenv').config()
-}
-export function getWVKPath() {
- if (isDev) {
- const key = process.env.KEY_KEY
- const client = process.env.KEY_CLIENT
+export async function getWVKPath() {
+ const drmL3blob = await settings.get('l3blob') as string
+ const drmL3key = await settings.get('l3key') as string
- return { key: key, client: client }
- } else {
- const key = path.join(keyPath, 'key')
- const client = path.join(keyPath, 'client')
-
- return { key: key, client: client }
+ if (!drmL3blob || !drmL3key) {
+ return
}
+
+ return { key: drmL3key, client: drmL3blob }
}
diff --git a/src/electron/background.ts b/src/electron/background.ts
index 7995eb9..00c4b41 100644
--- a/src/electron/background.ts
+++ b/src/electron/background.ts
@@ -167,6 +167,24 @@ ipcMain.handle('dialog:defaultDirectory', async () => {
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) => {
if (!type) return
diff --git a/src/electron/preload.ts b/src/electron/preload.ts
index 3b40017..0d4907d 100644
--- a/src/electron/preload.ts
+++ b/src/electron/preload.ts
@@ -3,6 +3,8 @@ import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('myAPI', {
selectFolder: () => ipcRenderer.invoke('dialog:openDirectory'),
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'),
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),