Skip to content

Commit

Permalink
Add support for viewing available download formats prior to downloading
Browse files Browse the repository at this point in the history
  • Loading branch information
m4heshd committed Jan 26, 2024
1 parent d89616a commit fc2bc3a
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ No installation is required on the desktop. The app is portable.
### Docker

⚠️ Currently, the docker image is only built for `linux/amd64` systems which means it cannot be run on ARM-based systems
like older Raspberry Pis.
like Raspberry Pis.

You can either build the docker image locally and run using [`docker-compose.yaml`](docker-compose.yaml) or use the
published [official image](https://hub.docker.com/r/m4heshd/ufc-ripper).
Expand Down
36 changes: 36 additions & 0 deletions server-modules/bin-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const bins = {
module.exports = {
binPath,
validateBins,
getListFormatsOutput,
openDLSession,
cancelDLSession,
openDLDir,
Expand All @@ -42,6 +43,41 @@ function validateBins(cb) {
}
}

function getListFormatsOutput(hls) {
return new Promise((resolve, reject) => {
const args = ['--print', '%(formats.:.{format_id,resolution,fps,tbr,vcodec,acodec})j', hls];
let output = {};

const yt_dlp = spawn(path.join(bins.ytDlp), args);

yt_dlp.on('error', (error) => {
console.error(`${getConfig('verboseLogging') ? error.stack : error}\n`);
reject(createUFCRError(error, 'Failed to start the yt-dlp process'));
});

yt_dlp.on('close', (code) => {
if (code === 0) {
if (Array.isArray(output) && output.length) {
resolve(output);
} else {
reject(createUFCRError('Response does not contain any streams. Please check the URL you entered'));
}
} else {
reject(createUFCRError(output, 'Format request process ended with error. Check the console for error information'));
}
});

yt_dlp.stdout.on('data', (data) => {
output = JSON.parse(data.toString());
});

yt_dlp.stderr.on('data', (data) => {
if (getConfig('verboseLogging')) console.error(data.toString());
output += data.toString();
});
});
}

function openDLSession(VOD, isRestart, cb) {
const {qID, title, hls, vodURL} = VOD;
const {
Expand Down
20 changes: 20 additions & 0 deletions server-modules/io-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function initIO(httpServer) {
socket.on('get-dlq', cb => cb(DLQ));
socket.on('login', login);
socket.on('verify-url', verifyVOD);
socket.on('get-formats', getFormats);
socket.on('download', downloadVOD);
socket.on('cancel-download', cancelDownload);
socket.on('clear-dlq', clearDLQ);
Expand Down Expand Up @@ -69,6 +70,14 @@ async function verifyVOD(url, cb) {
}
}

async function getFormats(url, cb) {
try {
await sendFormats(url, cb);
} catch (error) {
sendError(error, cb);
}
}

async function downloadVOD(VOD, isRestart, cb) {
try {
require('./bin-util').openDLSession(
Expand Down Expand Up @@ -149,6 +158,17 @@ function sendVODMeta(VOD, cb) {
});
}

async function sendFormats(url, cb) {
const VOD = await getVODMeta(url);
const hls = await getVODStream(VOD.id);
const formats = await require('./bin-util').getListFormatsOutput(hls);

cb({
title: VOD.title,
formats
});
}

function sendVODDownload(VOD, isRestart, cb) {
DLQ[VOD.qID] = {
...VOD,
Expand Down
2 changes: 2 additions & 0 deletions src/assets/styles/common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ body {
--failure: #fa3434;
--failure-bg: #3a0d06;
--warning: #fdd716;
--secondary-text: #ffde39;
--inactive-text: #7c7c7c;
}

.center-content {
Expand Down
125 changes: 125 additions & 0 deletions src/components/ModViewFormats.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<template>
<div
id="modViewFormats"
class="modal mod-view-formats"
>
<div class="modal-title">
<i>stock_media</i>
<h5>Available download formats</h5>
</div>

<div class="mod-view-formats__content">
<div class="vod-info">
<span class="vod-info__title">{{ modViewFormats.vodData.title }}:</span>
</div>

<article class="border round">
<div class="formats-table">
<table>
<thead>
<tr>
<th>Format ID</th>
<th>Resolution</th>
<th>Framerate</th>
<th>Bitrate</th>
<th>VCodec</th>
<th>ACodec</th>
</tr>
</thead>
<tbody>
<tr v-for="format in modViewFormats.vodData.formats">
<td :class="{ 'grayed-out': isInvalid(format.format_id)}">{{ format.format_id || 'N/A' }}</td>
<td :class="{ 'grayed-out': isInvalid(format.resolution)}">{{ format.resolution || 'N/A' }}</td>
<td :class="{ 'grayed-out': isInvalid(format.fps)}">{{ format.fps || 'N/A' }}</td>
<td :class="{ 'grayed-out': isInvalid(format.tbr)}">
{{ format.tbr ? Math.trunc(format.tbr) + 'k' : 'N/A' }}
</td>
<td :class="{ 'grayed-out': isInvalid(format.vcodec)}">{{ format.vcodec || 'N/A' }}</td>
<td :class="{ 'grayed-out': isInvalid(format.acodec)}">{{ format.acodec || 'N/A' }}</td>
</tr>
</tbody>
</table>
</div>
</article>

<div class="instructions">
You can use any of the above metadata as the custom download format in the configuration. Please read more about
format selection in the <a href="https://github.com/yt-dlp/yt-dlp#format-selection">yt-dlp documentation</a>
before using this feature.
</div>
</div>

<nav class="right-align">
<button data-ui="#modViewFormats">
<i>close</i>
<span>Close</span>
</button>
</nav>
</div>
</template>

<script setup>
// Store
import {useModViewFormatsStore} from '@/store/modViewFormats';
// Store
const modViewFormats = useModViewFormatsStore();
// Table
function isInvalid(data) {
return (data == null || data === 'none');
}
</script>

<style lang="scss">
.mod-view-formats {
width: 100%;
max-width: 700px;
max-height: 80%;
display: grid;
grid-template-rows: max-content minmax(0px, 1fr) max-content;
overflow: hidden;
&__content {
overflow-y: auto;
.vod-info {
margin: 25px 0;
&__title {
font-size: 24rem;
font-weight: bold;
}
}
.formats-table {
th {
font-size: 15rem;
font-weight: bold;
color: var(--primary);
border-bottom: 1px solid var(--primary);
}
td {
&:first-child {
color: var(--secondary-text);
}
&.grayed-out {
color: var(--inactive-text);
}
}
}
.instructions {
margin: 25px 0;
font-size: 15rem;
& > a {
color: var(--primary);
text-decoration: underline;
}
}
}
}
</style>
5 changes: 5 additions & 0 deletions src/modules/ws-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ export function useWSUtil() {
return emitPromise('verify-url', url);
}

function getFormats(url) {
return emitPromise('get-formats', url);
}

function downloadVOD(VOD, isRestart) {
return emitPromise('download', VOD, isRestart);
}
Expand Down Expand Up @@ -139,6 +143,7 @@ export function useWSUtil() {
saveConfig,
login,
verifyURL,
getFormats,
downloadVOD,
cancelDownload,
clearDLQ,
Expand Down
33 changes: 30 additions & 3 deletions src/pages/Landing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@
<i>download</i>
</button>

<button
class="square round large"
title="Get available formats"
:disabled="busy"
@click="onBtnGetFmtClick"
>
<i>stock_media</i>
</button>

<button
class="square round large"
title="Configuration"
Expand Down Expand Up @@ -86,6 +95,7 @@
></ModVODConfirm>
<ModConfig></ModConfig>
<ModBinDL></ModBinDL>
<ModViewFormats></ModViewFormats>

<!-- Overlay -->
<Overlay :vActive="store.ui.overlay"></Overlay>
Expand All @@ -97,20 +107,23 @@
import {ref, nextTick, onMounted} from 'vue';
// Store
import {useAppStore} from '@/store';
import {useModViewFormatsStore} from '@/store/modViewFormats';
// Modules
import {useWSUtil} from '@/modules/ws-util';
// Components
import VODCard from '@/components/VODCard.vue';
import ModVODConfirm from '@/components/ModVODConfirm.vue';
import ModConfig from '@/components/ModConfig.vue';
import Overlay from '@/components/Overlay.vue';
import ModBinDL from '@/components/ModBinDL.vue';
import ModViewFormats from '@/components/ModViewFormats.vue';
import Overlay from '@/components/Overlay.vue';
// Store
const store = useAppStore();
const modViewFormats = useModViewFormatsStore();
// Websocket
const {cancelDownload, clearDLQ, downloadVOD, initSocket, openDownloadsDir, verifyURL} = useWSUtil();
const {cancelDownload, clearDLQ, downloadVOD, initSocket, openDownloadsDir, verifyURL, getFormats} = useWSUtil();
initSocket();
Expand Down Expand Up @@ -138,7 +151,21 @@ function onBtnDownloadClick() {
.finally(switchBusyState);
}
// Downloads
function onBtnGetFmtClick() {
if (!store.isLoggedIn) return store.popError('You need to be logged in to check download formats');
switchBusyState();
getFormats(txtLink.value)
.then((res) => {
modViewFormats.setVODData(res);
window.ui('#modViewFormats');
})
.catch(store.popError)
.finally(switchBusyState);
}
// Downloads section
function onBtnOpenDLDir() {
openDownloadsDir().catch(store.popError);
}
Expand Down
13 changes: 13 additions & 0 deletions src/store/modViewFormats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Store
import {defineStore} from 'pinia';

export const useModViewFormatsStore = defineStore('modViewFormats', {
state: () => ({
vodData: {}
}),
actions: {
setVODData(newData) {
Object.assign(this.vodData, newData);
}
}
});

0 comments on commit fc2bc3a

Please sign in to comment.