diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ef3f9b3 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug Main Process", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + }, + "args" : ["."], + "outputCapture": "std" + } + ] + } \ No newline at end of file diff --git a/components/Settings/Naming.vue b/components/Settings/Naming.vue index cd97085..197a84a 100644 --- a/components/Settings/Naming.vue +++ b/components/Settings/Naming.vue @@ -9,19 +9,13 @@ placeholder="Episode Naming" class="bg-[#5c5b5b] w-full focus:outline-none px-3 py-2 rounded-xl text-sm text-center" /> -
- Example: -
+
Example:
{{ `${episodeNaming}` }}
-
- Variables: -
-
- {seriesName}, {seasonNumber}, {seasonNumberDD}, {episodeNumber}, {episodeNumberDD}, {quality} -
+
Variables:
+
{seriesName}, {seasonNumber}, {seasonNumberDD}, {episodeNumber}, {episodeNumberDD}, {quality}
Season Folder Naming
@@ -32,30 +26,24 @@ placeholder="Episode Naming" class="bg-[#5c5b5b] w-full focus:outline-none px-3 py-2 rounded-xl text-sm text-center" /> -
- Example: -
+
Example:
{{ `${seasonNaming}` }}
-
- Variables: -
-
- {seriesName}, {seasonNumber}, {seasonNumberDD}, {quality} -
+
Variables:
+
{seriesName}, {seasonNumber}, {seasonNumberDD}, {quality}
diff --git a/pages/index.vue b/pages/index.vue index 34ad988..01cdb3e 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -56,19 +56,35 @@ {{ p.status }} -
+
{{ p.status }}
-
+
{{ p.status }}
-
+
{{ p.status }}
-
+
+ + {{ p.status }} +
+
{{ p.status }}
@@ -77,10 +93,13 @@ {{ p.status }}
-
- - {{ a.status }} Audio {{ a.audio }} -
+
+ + {{ a.status }} Audio {{ a.audio }} +
@@ -141,9 +160,9 @@ const playlist = ref< service: string format: string audiosdownloading: { - status: string, - audio: string - }[] + status: string + audio: string + }[] }> >() @@ -165,7 +184,7 @@ const getPlaylist = async () => { service: string format: string audiosdownloading: { - status: string, + status: string audio: string }[] }> diff --git a/src/api/db/database.ts b/src/api/db/database.ts index 6517512..b0d8935 100644 --- a/src/api/db/database.ts +++ b/src/api/db/database.ts @@ -29,7 +29,20 @@ interface AccountCreateAttributes { interface PlaylistAttributes { id: number - status: 'waiting' | 'preparing' | 'waiting for playlist' | 'waiting for sub playlist' | 'waiting for dub playlist' | 'downloading' | 'merging video' | 'decrypting video' | 'awaiting all dubs downloaded' | 'merging video & audio' | 'completed' | 'failed' + status: + | 'waiting' + | 'preparing' + | 'waiting for playlist' + | 'waiting for sub playlist' + | 'waiting for dub playlist' + | 'downloading' + | 'downloading video' + | 'merging video' + | 'decrypting video' + | 'awaiting all dubs downloaded' + | 'merging video & audio' + | 'completed' + | 'failed' media: CrunchyEpisode | ADNEpisode dub: Array sub: Array @@ -49,7 +62,20 @@ interface PlaylistCreateAttributes { dir: string quality: 1080 | 720 | 480 | 360 | 240 hardsub: boolean - status: 'waiting' | 'preparing' | 'waiting for playlist' | 'waiting for sub playlist' | 'waiting for dub playlist' | 'downloading' | 'merging video' | 'decrypting video' | 'awaiting all dubs downloaded' | 'merging video & audio' | 'completed' | 'failed' + status: + | 'waiting' + | 'preparing' + | 'waiting for playlist' + | 'waiting for sub playlist' + | 'waiting for dub playlist' + | 'downloading' + | 'downloading video' + | 'merging video' + | 'decrypting video' + | 'awaiting all dubs downloaded' + | 'merging video & audio' + | 'completed' + | 'failed' service: 'CR' | 'ADN' format: 'mp4' | 'mkv' } diff --git a/src/api/routes/crunchyroll/crunchyroll.service.ts b/src/api/routes/crunchyroll/crunchyroll.service.ts index 634a632..e24478a 100644 --- a/src/api/routes/crunchyroll/crunchyroll.service.ts +++ b/src/api/routes/crunchyroll/crunchyroll.service.ts @@ -17,10 +17,14 @@ const crErrors = [ // Crunchyroll Login Handler export async function crunchyLogin(user: string, passw: string, geo: string) { var endpoint = await settings.get('CREndpoint') - const drmL3blob = await settings.get('l3blob') - const drmL3key = await settings.get('l3key') + const drmL3blob = await settings.has('l3blob') + const drmL3key = await settings.has('l3key') if (!drmL3blob || !drmL3key) { + endpoint = 1 + } + + if (!endpoint) { await settings.set('CREndpoint', 1) endpoint = 1 } @@ -71,8 +75,8 @@ async function crunchyLoginFetchProxy(user: string, passw: string, geo: string) var body var endpoint = await settings.get('CREndpoint') - const drmL3blob = await settings.get('l3blob') - const drmL3key = await settings.get('l3key') + const drmL3blob = await settings.has('l3blob') + const drmL3key = await settings.has('l3key') var proxy: | { name: string @@ -189,10 +193,14 @@ async function crunchyLoginFetch(user: string, passw: string) { var body var endpoint = await settings.get('CREndpoint') - const drmL3blob = await settings.get('l3blob') - const drmL3key = await settings.get('l3key') + const drmL3blob = await settings.has('l3blob') + const drmL3key = await settings.has('l3key') if (!drmL3blob || !drmL3key) { + endpoint = 1 + } + + if (!endpoint) { await settings.set('CREndpoint', 1) endpoint = 1 } @@ -274,6 +282,27 @@ async function crunchyLoginFetch(user: string, passw: string) { } } +let counter = 0 +var maxLimit = 0 + +async function incrementPlaylistCounter() { + return new Promise((resolve) => { + const interval = setInterval(() => { + if (counter < maxLimit) { + counter++ + clearInterval(interval) + resolve() + } + }, 100) + }) +} + +function decrementPlaylistCounter() { + if (counter > 0) { + counter-- + } +} + // Crunchyroll Playlist Fetch export async function crunchyGetPlaylist(q: string, geo: string | undefined) { const isProxyActive = await settings.get('proxyActive') @@ -290,10 +319,14 @@ export async function crunchyGetPlaylist(q: string, geo: string | undefined) { } var endpoint = await settings.get('CREndpoint') - const drmL3blob = await settings.get('l3blob') - const drmL3key = await settings.get('l3key') + const drmL3blob = await settings.has('l3blob') + const drmL3key = await settings.has('l3key') if (!drmL3blob || !drmL3key) { + endpoint = 1 + } + + if (!endpoint) { await settings.set('CREndpoint', 1) endpoint = 1 } @@ -383,6 +416,12 @@ export async function crunchyGetPlaylist(q: string, geo: string | undefined) { var playlist: VideoPlaylist + if (maxLimit === 0) { + maxLimit = await checkAccountMaxStreams() + } + + await incrementPlaylistCounter() + try { const response = await fetch( `https://cr-play-service.prd.crunchyrollsvc.com/v1/${q}${ @@ -406,44 +445,12 @@ export async function crunchyGetPlaylist(q: string, geo: string | undefined) { playlist = data } else { const error = await response.text() - const errorJSON: { - activeStreams: { - accountId: string - active: boolean - assetId: string - clientId: string - contentId: string - country: string - createdTimestamp: string - deviceSubtype: string - deviceType: string - episodeIdentity: string - id: string - token: string - }[] - } = await JSON.parse(error) - - if (errorJSON && errorJSON.activeStreams && errorJSON.activeStreams.length !== 0) { - for (const e of errorJSON.activeStreams) { - await deleteVideoToken(e.contentId, e.token) - } - - server.logger.log({ - level: 'error', - message: 'Refetching Crunchyroll Video Playlist & Deleting all Video Token because too many streams', - error: errorJSON, - timestamp: new Date().toISOString(), - section: 'playlistCrunchyrollFetch' - }) - - return await crunchyGetPlaylist(q, geo) - } messageBox('error', ['Cancel'], 2, 'Failed to get Crunchyroll Video Playlist', 'Failed to get Crunchyroll Video Playlist', error) server.logger.log({ level: 'error', message: 'Failed to get Crunchyroll Video Playlist', - error: errorJSON, + error: error, timestamp: new Date().toISOString(), section: 'playlistCrunchyrollFetch' }) @@ -466,6 +473,8 @@ export async function crunchyGetPlaylist(q: string, geo: string | undefined) { 'User-Agent': 'Crunchyroll/1.8.0 Nintendo Switch/12.3.12.0 UE4/4.27' } + await incrementPlaylistCounter() + const responseProx = await fetch( `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' @@ -502,6 +511,8 @@ export async function crunchyGetPlaylist(q: string, geo: string | undefined) { } await deleteVideoToken(q, dataProx.token) + } else { + decrementPlaylistCounter(); } } } @@ -536,6 +547,9 @@ export async function deleteVideoToken(content: string, token: string) { timestamp: new Date().toISOString(), section: 'tokenDeletionCrunchyrollFetch' }) + + decrementPlaylistCounter() + return 'ok' } else { const error = await response.text() @@ -605,7 +619,7 @@ export async function getAccountInfo() { if (!login) return const headers = { - Authorization: `Bearer ${login.access_token}`, + Authorization: `Bearer ${login.access_token}` } try { @@ -616,8 +630,8 @@ export async function getAccountInfo() { if (response.ok) { const data: { - account_id: string, - external_id: string, + account_id: string + external_id: string } = await JSON.parse(await response.text()) return data @@ -640,7 +654,6 @@ export async function getAccountInfo() { // Check Max account streams because of crunchyroll activestream limit export async function checkAccountMaxStreams() { - const accountinfo = await getAccountInfo() if (!accountinfo) return 1 @@ -654,7 +667,7 @@ export async function checkAccountMaxStreams() { if (!login) return 1 const headers = { - Authorization: `Bearer ${login.access_token}`, + Authorization: `Bearer ${login.access_token}` } try { @@ -666,22 +679,22 @@ export async function checkAccountMaxStreams() { if (response.ok) { const data: { items: { - __class__: string, - __href__: string, - __links__: string, - __actions__: string, - benefit: string, + __class__: string + __href__: string + __links__: string + __actions__: string + benefit: string source: string }[] } = await JSON.parse(await response.text()) if (!data.items || data.items.length === 0) return 1 - if (data.items.find(i => i.benefit === 'concurrent_streams.4')) return 2 + if (data.items.find((i) => i.benefit === 'concurrent_streams.4')) return 2 - if (data.items.find(i => i.benefit === 'concurrent_streams.1')) return 1 + if (data.items.find((i) => i.benefit === 'concurrent_streams.1')) return 1 - if (data.items.find(i => i.benefit === 'concurrent_streams.6')) return 3 + if (data.items.find((i) => i.benefit === 'concurrent_streams.6')) return 3 return 1 } else { @@ -699,4 +712,4 @@ export async function checkAccountMaxStreams() { } 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 c0b75c2..5fbfc12 100644 --- a/src/api/routes/service/service.service.ts +++ b/src/api/routes/service/service.service.ts @@ -171,6 +171,7 @@ export async function updatePlaylistByID( | 'waiting for sub playlist' | 'waiting for dub playlist' | 'downloading' + | 'downloading video' | 'merging video' | 'decrypting video' | 'awaiting all dubs downloaded' @@ -193,7 +194,7 @@ export async function updatePlaylistByID( section: 'playlistItemUpdateDatabase' }) } catch (e) { - messageBox('error', ['Cancel'], 2, 'Database Error', 'Failed to update playlist item', JSON.stringify(e)) + messageBox('error', ['Cancel'], 2, 'Database Error', 'Failed to update playlist item', 'Failed to update playlist item') server.logger.log({ level: 'error', message: 'Failed to update playlist item', @@ -218,6 +219,7 @@ export async function addEpisodeToPlaylist( | 'waiting for sub playlist' | 'waiting for dub playlist' | 'downloading' + | 'downloading video' | 'merging video' | 'decrypting video' | 'awaiting all dubs downloaded' @@ -451,27 +453,6 @@ export async function downloadADNPlaylist( await deleteFolder(videoFolder) } -var counter = 0 -var maxLimit = 1 - -async function incrementPlaylistCounter() { - return new Promise((resolve) => { - const interval = setInterval(() => { - if (counter < maxLimit) { - counter++ - clearInterval(interval) - resolve() - } - }, 100) - }) -} - -function decrementPlaylistCounter() { - if (counter > 0) { - counter-- - } -} - // Download Crunchyroll Playlist export async function downloadCrunchyrollPlaylist( e: string, @@ -488,15 +469,8 @@ export async function downloadCrunchyrollPlaylist( format: 'mp4' | 'mkv', geo: string | undefined ) { - const accmaxstream = await checkAccountMaxStreams() - - if (accmaxstream) { - maxLimit = accmaxstream - } - await updatePlaylistByID(downloadID, 'waiting for playlist') - await incrementPlaylistCounter() var playlist = await crunchyGetPlaylist(e, geo) if (!playlist) { @@ -517,8 +491,6 @@ export async function downloadCrunchyrollPlaylist( const found = playlist.data.versions.find((v) => v.audio_locale === 'ja-JP') if (found) { await deleteVideoToken(episodeID, playlist.data.token) - decrementPlaylistCounter() - await incrementPlaylistCounter() playlist = await crunchyGetPlaylist(found.guid, found.geo) } else { console.log('Exact Playlist not found, taking what crunchy gives.') @@ -547,7 +519,6 @@ export async function downloadCrunchyrollPlaylist( } await deleteVideoToken(episodeID, playlist.data.token) - decrementPlaylistCounter() const subFolder = await createFolder() @@ -600,7 +571,6 @@ export async function downloadCrunchyrollPlaylist( if (playlist.data.audioLocale !== 'ja-JP') { const foundStream = playlist.data.versions.find((v) => v.audio_locale === 'ja-JP') if (foundStream) { - await incrementPlaylistCounter() subPlaylist = await crunchyGetPlaylist(foundStream.guid, foundStream.geo) } } else { @@ -641,7 +611,6 @@ export async function downloadCrunchyrollPlaylist( } await deleteVideoToken(episodeID, subPlaylist.data.token) - decrementPlaylistCounter() } await updatePlaylistByID(downloadID, 'waiting for dub playlist') @@ -653,11 +622,9 @@ export async function downloadCrunchyrollPlaylist( } if (found) { - await incrementPlaylistCounter() const list = await crunchyGetPlaylist(found.guid, found.geo) if (list) { await deleteVideoToken(episodeID, list.data.token) - decrementPlaylistCounter() const foundSub = list.data.subtitles.find((sub) => sub.language === d) if (foundSub) { @@ -699,7 +666,7 @@ export async function downloadCrunchyrollPlaylist( } } - await updatePlaylistByID(downloadID, 'downloading') + await updatePlaylistByID(downloadID, 'downloading video') const subDownload = async () => { const sbs: Array = [] @@ -713,7 +680,6 @@ export async function downloadCrunchyrollPlaylist( const audioDownload = async () => { const audios: Array = [] for (const v of dubDownloadList) { - await incrementPlaylistCounter() const list = await crunchyGetPlaylist(v.guid, v.geo) if (!list) return @@ -723,7 +689,6 @@ export async function downloadCrunchyrollPlaylist( if (!playlist) return await deleteVideoToken(episodeID, list.data.token) - decrementPlaylistCounter() const assetId = playlist.mediaGroups.AUDIO.audio.main.playlists[0].segments[0].resolvedUri.match(/\/assets\/(?:p\/)?([^_,]+)/) @@ -837,7 +802,6 @@ export async function downloadCrunchyrollPlaylist( return } - await incrementPlaylistCounter() const play = await crunchyGetPlaylist(code, geo) if (!play) { @@ -882,7 +846,6 @@ export async function downloadCrunchyrollPlaylist( if (!mdp) return await deleteVideoToken(episodeID, play.data.token) - decrementPlaylistCounter() var hq = mdp.playlists.find((i) => i.attributes.RESOLUTION?.height === quality) diff --git a/src/api/services/folder.ts b/src/api/services/folder.ts index f4c8718..0918982 100644 --- a/src/api/services/folder.ts +++ b/src/api/services/folder.ts @@ -4,8 +4,7 @@ import fs from 'fs' import settings from 'electron-settings' export async function createFolder() { - - var tempPath = await settings.get('tempPath') as string + var tempPath = (await settings.get('tempPath')) as string if (!tempPath) { tempPath = app.getPath('temp') @@ -33,8 +32,7 @@ export async function checkDirectoryExistence(dir: string) { } export async function createFolderName(name: string, dir: string) { - - var tempPath = await settings.get('tempPath') as string + var tempPath = (await settings.get('tempPath')) as string if (!tempPath) { tempPath = app.getPath('temp') @@ -69,8 +67,7 @@ export async function deleteFolder(folderPath: string) { } export async function deleteTemporaryFolders() { - - var tempPath = await settings.get('tempPath') as string + var tempPath = (await settings.get('tempPath')) as string if (!tempPath) { tempPath = app.getPath('temp') diff --git a/src/electron/background.ts b/src/electron/background.ts index 1c593f6..d3f7d02 100644 --- a/src/electron/background.ts +++ b/src/electron/background.ts @@ -18,6 +18,7 @@ var mainWindow: BrowserWindow function createWindow() { console.log('System info', { isProduction, platform, architucture }) + mainWindow = new BrowserWindow({ title: 'Crunchyroll Downloader', icon: __dirname + '/icon/favicon.ico', @@ -73,6 +74,11 @@ function createWindow() { // App events // ========== app.whenReady().then(async () => { + settings.configure({ + dir: app.getPath('documents') + '/Crunchyroll Downloader/settings/', + atomicSave: process.platform !== 'win32' + }) + startAPI() const mainWindow = createWindow() diff --git a/src/electron/preload.ts b/src/electron/preload.ts index 5e4b102..c03deb9 100644 --- a/src/electron/preload.ts +++ b/src/electron/preload.ts @@ -22,5 +22,5 @@ contextBridge.exposeInMainWorld('myAPI', { setEpisodeTemplate: (name: string) => ipcRenderer.invoke('dialog:setEpisodeTemplate', name), getEpisodeTemplate: () => ipcRenderer.invoke('dialog:getEpisodeTemplate'), setSeasonTemplate: (name: string) => ipcRenderer.invoke('dialog:setSeasonTemplate', name), - getSeasonTemplate: () => ipcRenderer.invoke('dialog:getSeasonTemplate'), + getSeasonTemplate: () => ipcRenderer.invoke('dialog:getSeasonTemplate') })