init
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import os
|
import os
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
django_asgi_app = get_asgi_application()
|
||||||
|
|
||||||
from channels.auth import AuthMiddlewareStack
|
from channels.auth import AuthMiddlewareStack
|
||||||
from channels.sessions import SessionMiddlewareStack
|
from channels.sessions import SessionMiddlewareStack
|
||||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||||
@@ -8,8 +10,7 @@ from channels.routing import ProtocolTypeRouter, URLRouter
|
|||||||
from app.channels_middleware import JwtOrSessionAuthMiddleware
|
from app.channels_middleware import JwtOrSessionAuthMiddleware
|
||||||
|
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')
|
|
||||||
django_asgi_app = get_asgi_application()
|
|
||||||
|
|
||||||
from .ws_urls import websocket_urlpatterns
|
from .ws_urls import websocket_urlpatterns
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,6 @@ INSTALLED_APPS = [
|
|||||||
'api',
|
'api',
|
||||||
'torrent',
|
'torrent',
|
||||||
]
|
]
|
||||||
if DEBUG:
|
|
||||||
INSTALLED_APPS = ["daphne"] + INSTALLED_APPS
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from django.http import StreamingHttpResponse
|
|||||||
import zlib
|
import zlib
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
import aiofiles
|
import anyio
|
||||||
from stat import S_IFREG
|
from stat import S_IFREG
|
||||||
from stream_zip import ZIP_64, stream_zip, async_stream_zip
|
from stream_zip import ZIP_64, stream_zip, async_stream_zip
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
@@ -61,7 +61,7 @@ class StreamingZipFileResponse(StreamingHttpResponse):
|
|||||||
|
|
||||||
async def contents(path):
|
async def contents(path):
|
||||||
try:
|
try:
|
||||||
async with aiofiles.open(path, "rb") as f:
|
async with await anyio.open_file(path, "rb") as f:
|
||||||
while chunk := await f.read(64 * 1024):
|
while chunk := await f.read(64 * 1024):
|
||||||
yield chunk
|
yield chunk
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
CORES=$(nproc)
|
||||||
|
WORKERS=$((2 * CORES + 1))
|
||||||
|
|
||||||
cd /app/frontend && yarn dev &
|
cd /app/frontend && yarn dev &
|
||||||
cd /app && python manage.py runserver 0.0.0.0:8000 &
|
cd /app && uvicorn app.asgi:application --workers $WORKERS --host 0.0.0.0 --port 8000 --lifespan off --loop uvloop --ws websockets --reload &
|
||||||
|
|
||||||
wait -n
|
wait -n
|
||||||
|
|
||||||
|
|||||||
@@ -11,24 +11,26 @@
|
|||||||
</v-row>
|
</v-row>
|
||||||
</v-list-item-action>
|
</v-list-item-action>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
<v-divider/>
|
|
||||||
</v-list>
|
</v-list>
|
||||||
|
<v-divider/>
|
||||||
<UserStats :stats="user_stats"/>
|
<UserStats :stats="user_stats"/>
|
||||||
|
<v-divider/>
|
||||||
|
{{dm_status}}
|
||||||
|
<DM v-if="dm_activated" :dm_files="dm_files" :dm_status="dm_status" :dm_download_location="dm_download_location"/>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
<v-navigation-drawer v-model="right_drawer" location="right" temporary>
|
<v-navigation-drawer v-model="right_drawer" location="right" temporary>
|
||||||
<Friend v-if="right_drawer" :active_user="display_user.id" @userSelected="userSelectedUpdated"/>
|
<Friend v-if="right_drawer" :active_user="display_user.id" @userSelected="userSelectedUpdated"/>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
<v-app-bar app>
|
<v-app-bar>
|
||||||
<v-app-bar-title>
|
<v-app-bar-title style="max-width: 150px">
|
||||||
<v-row align="center" justify="start" style="max-width: 100%">
|
|
||||||
<v-col class="v-col-1 text-center">
|
|
||||||
<v-btn variant="plain" href="/">Oxpanel</v-btn>
|
<v-btn variant="plain" href="/">Oxpanel</v-btn>
|
||||||
</v-col>
|
</v-app-bar-title>
|
||||||
<v-col class="v-col-3">
|
<v-row align="center" justify="center" style="max-width: 500px" class="mx-auto">
|
||||||
<v-text-field flat solo-inverted hide-details prepend-inner-icon="mdi-magnify" label="Search" v-model="filters.search" clearable @click:clear="filters.search = ''"/>
|
<v-col>
|
||||||
|
<v-text-field density="comfortable" flat solo-inverted hide-details prepend-inner-icon="mdi-magnify" label="Search" v-model="filters.search" clearable @click:clear="filters.search = ''"/>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-app-bar-title>
|
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-menu>
|
<v-menu>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
@@ -67,6 +69,7 @@ import TorrentList from "@/components/torrent/TorrentList.vue";
|
|||||||
import UploadForm from "@/components/torrent/UploadForm.vue";
|
import UploadForm from "@/components/torrent/UploadForm.vue";
|
||||||
import Friend from "@/components/auth/Friend.vue";
|
import Friend from "@/components/auth/Friend.vue";
|
||||||
import UserStats from "@/components/torrent/UserStats.vue";
|
import UserStats from "@/components/torrent/UserStats.vue";
|
||||||
|
import DM from "@/components/torrent/DM.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -83,18 +86,23 @@ export default {
|
|||||||
filters: {},
|
filters: {},
|
||||||
torrents: [],
|
torrents: [],
|
||||||
user_stats: {
|
user_stats: {
|
||||||
"torrents_size": 0,
|
torrents_size: 0,
|
||||||
"torrents_len": 0,
|
torrents_len: 0,
|
||||||
"torrent_len_shared": 0,
|
torrent_len_shared: 0,
|
||||||
"torrents_total_len": 0,
|
torrents_total_len: 0,
|
||||||
"user_max_size": 0,
|
user_max_size: 0,
|
||||||
"user_usage_percent": 0,
|
user_usage_percent: 0,
|
||||||
"disk_total": 0,
|
disk_total: 0,
|
||||||
"disk_used": 0,
|
disk_used: 0,
|
||||||
"disk_free": 0,
|
disk_free: 0,
|
||||||
"disk_usage_percent": 0,
|
disk_usage_percent: 0,
|
||||||
},
|
},
|
||||||
filter_timer: null,
|
filter_timer: null,
|
||||||
|
dm_files: {},
|
||||||
|
// dm_stats: {},
|
||||||
|
dm_status: {},
|
||||||
|
dm_download_location: "",
|
||||||
|
dm_activated: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -134,6 +142,29 @@ export default {
|
|||||||
})
|
})
|
||||||
await this.fetchTorrents();
|
await this.fetchTorrents();
|
||||||
await this.fetchUserStats();
|
await this.fetchUserStats();
|
||||||
|
if(this.$qt.connect()){
|
||||||
|
this.$qt.on("ready", () => {
|
||||||
|
this.dm_status = this.$qt.getProperty("dm_status");
|
||||||
|
this.dm_files = this.$qt.getProperty("dm_files");
|
||||||
|
// this.dm_stats = this.$qt.getProperty("dm_stats");
|
||||||
|
this.dm_download_location = this.$qt.getProperty("dm_download_location");
|
||||||
|
this.dm_activated = true;
|
||||||
|
// console.log(JSON.stringify(this.dm_files))
|
||||||
|
|
||||||
|
this.$qt.on("status_updated", data => {
|
||||||
|
this.dm_status = data;
|
||||||
|
})
|
||||||
|
// this.$qt.on("stats_updated", data => {
|
||||||
|
// this.dm_stats = data;
|
||||||
|
// })
|
||||||
|
this.$qt.on("files_updated", data => {
|
||||||
|
this.dm_files = data;
|
||||||
|
})
|
||||||
|
this.$qt.on("download_location_updated", data => {
|
||||||
|
this.dm_download_location = data;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async fetchUserStats(){
|
async fetchUserStats(){
|
||||||
@@ -219,3 +250,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
75
app/frontend/src/components/torrent/DM.vue
Normal file
75
app/frontend/src/components/torrent/DM.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item prepend-icon="mdi-details" @click="details_enabled = true" title="Download Manager"/>
|
||||||
|
<v-dialog width="80%" v-model="details_enabled">
|
||||||
|
<DMDetails :dm_files="dm_files" :dm_status="dm_status" :dm_download_location="dm_download_location"/>
|
||||||
|
</v-dialog>
|
||||||
|
<v-list-item
|
||||||
|
:prepend-icon="dm_status.pause ? 'mdi-play' : 'mdi-pause'"
|
||||||
|
@click="$qt.callMethod('set_pause', !dm_status.pause)"
|
||||||
|
>
|
||||||
|
<template v-slot:title>
|
||||||
|
{{ dm_status.pause ? 'Start':'Pause' }}
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item prepend-icon="mdi-speedometer">
|
||||||
|
<template v-slot:title>
|
||||||
|
{{fs_speed_format(dm_status.speed)}}/s
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item prepend-icon="mdi-file-download" >
|
||||||
|
<template v-slot:title>
|
||||||
|
{{dm_status.downloaded_files}}/{{dm_status.total_files}}
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item prepend-icon="mdi-tape-drive">
|
||||||
|
<template v-slot:title>
|
||||||
|
{{fs_format(dm_status.downloaded_size)}}/{{fs_format(dm_status.total_size)}}
|
||||||
|
</template>
|
||||||
|
<template v-slot:subtitle>
|
||||||
|
<v-progress-linear :model-value="percentDownloaded" height="12" color="green">{{percentDownloaded.toFixed(1)}}%</v-progress-linear>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import DMDetails from "@/components/torrent/DMDetails.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {fs_format, fs_speed_format} from "@/plugins/utils.js";
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {fs_format, fs_speed_format},
|
||||||
|
props: {
|
||||||
|
dm_files: {
|
||||||
|
required: true,
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
dm_status: {
|
||||||
|
required: true,
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
dm_download_location: {
|
||||||
|
required: true,
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
details_enabled: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
percentDownloaded(){
|
||||||
|
return this.dm_status.total_size > 0 ? this.dm_status.downloaded_size / this.dm_status.total_size * 100: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
</style>
|
||||||
123
app/frontend/src/components/torrent/DMDetails.vue
Normal file
123
app/frontend/src/components/torrent/DMDetails.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<v-card class="dm-details-card">
|
||||||
|
<v-card-actions>
|
||||||
|
<v-text-field :model-value="dm_download_location" readonly @click="$qt.callMethod('change_path')"></v-text-field>
|
||||||
|
</v-card-actions>
|
||||||
|
<v-divider/>
|
||||||
|
<v-card-text class="scrollable-content">
|
||||||
|
<!-- Liste des fichiers en téléchargement -->
|
||||||
|
<div v-if="Object.keys(filterFiles.downloading).length > 0">
|
||||||
|
<h3>Fichiers en téléchargement</h3>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item v-for="(file, key) in filterFiles.downloading" :key="key">
|
||||||
|
<template v-slot:title>
|
||||||
|
{{ file.rel_path }}
|
||||||
|
</template>
|
||||||
|
<template v-slot:subtitle>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
<!-- Liste des fichiers en attente -->
|
||||||
|
<div>
|
||||||
|
<h3>Fichiers en attente</h3>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item v-for="(file, key) in filterFiles.waiting" :key="key">
|
||||||
|
<template v-slot:title>
|
||||||
|
{{ file.rel_path }}
|
||||||
|
</template>
|
||||||
|
<template v-slot:subtitle>
|
||||||
|
En attente
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Liste des fichiers terminés -->
|
||||||
|
<div>
|
||||||
|
<h3>Fichiers terminés</h3>
|
||||||
|
<v-list density="compact">
|
||||||
|
<v-list-item v-for="(file, key) in filterFiles.finished" :key="key">
|
||||||
|
<template v-slot:title>
|
||||||
|
{{ file.rel_path }}
|
||||||
|
</template>
|
||||||
|
<template v-slot:subtitle>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {fs_format, fs_speed_format} from "@/plugins/utils.js";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
props: {
|
||||||
|
dm_files: {
|
||||||
|
required: true,
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
dm_status: {
|
||||||
|
required: true,
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
dm_download_location: {
|
||||||
|
required: true,
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data(){
|
||||||
|
return {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted(){
|
||||||
|
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filterFiles(){
|
||||||
|
let waiting = {},
|
||||||
|
downloading = {},
|
||||||
|
finished = {};
|
||||||
|
|
||||||
|
for(const [file_id, file] of Object.entries(this.dm_files)){
|
||||||
|
if(file.downloaded){
|
||||||
|
finished[file_id] = file;
|
||||||
|
}else if(file_id in this.dm_status.downloading){
|
||||||
|
downloading[file_id] = file;
|
||||||
|
}else{
|
||||||
|
waiting[file_id] = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {waiting, downloading, finished};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.scrollable-content {
|
||||||
|
max-height: 400px; /* Définissez la hauteur maximale souhaitée */
|
||||||
|
overflow-y: auto; /* Active le défilement vertical quand nécessaire */
|
||||||
|
overflow-x: hidden; /* Empêche le défilement horizontal */
|
||||||
|
padding-right: 8px; /* Espace pour la barre de défilement */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-details-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -44,11 +44,16 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
downloadClicked(){
|
downloadClicked(){
|
||||||
if(!this.is_download_finished) return;
|
if(!this.is_download_finished) return;
|
||||||
|
if(this.$qt.is_active){
|
||||||
|
this.$qt.callMethod("add_files", this.file);
|
||||||
|
}else{
|
||||||
let a = document.createElement("a");
|
let a = document.createElement("a");
|
||||||
a.href = this.file.download_url;
|
a.href = this.file.download_url;
|
||||||
a.setAttribute("download", "download");
|
a.setAttribute("download", "download");
|
||||||
a.click();
|
a.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -42,6 +42,7 @@ export default {
|
|||||||
let response = await fetch(`/api/torrent/files?${new URLSearchParams(this.filters)}`);
|
let response = await fetch(`/api/torrent/files?${new URLSearchParams(this.filters)}`);
|
||||||
this.files = await response.json();
|
this.files = await response.json();
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
return this.files;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
<v-card @click="show_files = !show_files">
|
<v-card @click="show_files = !show_files">
|
||||||
<v-progress-linear height="20" :color="progressData.color" :model-value="progressData.value">
|
<v-progress-linear height="20" :color="progressData.color" :model-value="progressData.value">
|
||||||
<!-- barre de progression -->
|
<!-- barre de progression -->
|
||||||
<v-progress-circular
|
<!-- <v-progress-circular-->
|
||||||
v-if="!torrent.transmission_data.rateDownload && !torrent.transmission_data.rateUpload"
|
<!-- v-if="!torrent.transmission_data.rateDownload && !torrent.transmission_data.rateUpload"-->
|
||||||
size="15"
|
<!-- size="15"-->
|
||||||
indeterminate
|
<!-- indeterminate-->
|
||||||
:color="torrent.transmission_data.progress < 100 ? 'green':'red'"
|
<!-- :color="torrent.transmission_data.progress < 100 ? 'green':'red'"-->
|
||||||
/>
|
<!-- />-->
|
||||||
<v-icon
|
<v-icon
|
||||||
v-else
|
v-if="torrent.transmission_data.rateDownload && torrent.transmission_data.rateUpload"
|
||||||
:color="torrent.transmission_data.rateDownload ? 'green':'red'"
|
:color="torrent.transmission_data.rateDownload ? 'green':'red'"
|
||||||
:icon="torrent.transmission_data.rateDownload ? 'mdi-arrow-down-bold':'mdi-arrow-up-bold'"
|
:icon="torrent.transmission_data.rateDownload ? 'mdi-arrow-down-bold':'mdi-arrow-up-bold'"
|
||||||
/>
|
/>
|
||||||
@@ -52,7 +52,11 @@
|
|||||||
<v-expand-transition>
|
<v-expand-transition>
|
||||||
<div v-if="show_files">
|
<div v-if="show_files">
|
||||||
<v-divider />
|
<v-divider />
|
||||||
<FileList v-if="show_files" :torrent_id="torrent.id" :is_download_finished="torrent.transmission_data.progress >= 100"/>
|
<FileList
|
||||||
|
v-if="show_files"
|
||||||
|
:torrent_id="torrent.id"
|
||||||
|
:is_download_finished="torrent.transmission_data.progress >= 100"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</v-expand-transition>
|
</v-expand-transition>
|
||||||
</v-card>
|
</v-card>
|
||||||
@@ -88,10 +92,16 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
async downloadClicked(){
|
async downloadClicked(){
|
||||||
if(this.torrent.transmission_data.progress < 100) return;
|
if(this.torrent.transmission_data.progress < 100) return;
|
||||||
|
if(this.$qt.is_active){
|
||||||
|
let response = await fetch(`/api/torrent/files/?torrent=${this.torrent.id}`);
|
||||||
|
let files = await response.json();
|
||||||
|
this.$qt.callMethod("add_files", files);
|
||||||
|
}else{
|
||||||
let a = document.createElement("a");
|
let a = document.createElement("a");
|
||||||
a.href = this.torrent.download_url;
|
a.href = this.torrent.download_url;
|
||||||
a.setAttribute("download", "download");
|
a.setAttribute("download", "download");
|
||||||
a.click();
|
a.click();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async deleteClicked(){
|
async deleteClicked(){
|
||||||
this.$emit("delete", this.torrent.id);
|
this.$emit("delete", this.torrent.id);
|
||||||
|
|||||||
13
app/frontend/src/plugins/params.js
Normal file
13
app/frontend/src/plugins/params.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { reactive } from "vue";
|
||||||
|
|
||||||
|
class Params {
|
||||||
|
constructor() {
|
||||||
|
this.dl_hotfix_enabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install: (app, options) => {
|
||||||
|
app.config.globalProperties.$params = reactive(new Params());
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/frontend/src/plugins/qtwebchannel.js
Normal file
76
app/frontend/src/plugins/qtwebchannel.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
class QtWebChannel {
|
||||||
|
constructor() {
|
||||||
|
this.handler = null;
|
||||||
|
this.eventEmitter = new EventEmitter(); // Ajouter l'EventEmitter
|
||||||
|
}
|
||||||
|
|
||||||
|
get is_active(){
|
||||||
|
return Boolean(this.handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if(typeof QWebChannel !== 'undefined') {
|
||||||
|
this.handler = new QWebChannel(qt.webChannelTransport, channel => {
|
||||||
|
channel.objects.handler.on_message.connect((message) => {
|
||||||
|
this.handleMessage(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.callMethod('on_site_ready', args => {
|
||||||
|
this.eventEmitter.emit('ready');
|
||||||
|
})
|
||||||
|
console.log(this.getProperty("dm_status"))
|
||||||
|
})
|
||||||
|
return true;
|
||||||
|
}else{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callMethod(function_name, ...args){
|
||||||
|
console.log("call method", function_name, args)
|
||||||
|
if(this.handler !== null){
|
||||||
|
return this.handler.objects.handler[function_name](...args)
|
||||||
|
}else{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getProperty(property_name, default_value){
|
||||||
|
if(this.handler !== null){
|
||||||
|
let property = this.handler.objects.handler[property_name];
|
||||||
|
if(property !== undefined){
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return default_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage(message){
|
||||||
|
try {
|
||||||
|
if("context" in message){
|
||||||
|
this.eventEmitter.emit(message.context, message.content);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse message as JSON:', error);
|
||||||
|
this.eventEmitter.emit('invalid_message', { raw: message, error });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event, callback) {
|
||||||
|
this.eventEmitter.on(event, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
off(event, callback) {
|
||||||
|
this.eventEmitter.off(event, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install: (app, options) => {
|
||||||
|
app.config.globalProperties.$qt = new QtWebChannel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import Vuetify from "./vuetify"
|
import Vuetify from "./vuetify"
|
||||||
import Ws from "./ws";
|
import Ws from "./ws";
|
||||||
|
import QtWC from "./qtwebchannel.js"
|
||||||
|
|
||||||
export function createVue(component, dom_id){
|
export function createVue(component, dom_id){
|
||||||
return createApp(component).use(Vuetify).use(Ws).mount(dom_id)
|
return createApp(component).use(Vuetify).use(Ws).use(QtWC).mount(dom_id)
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ class WebSocketClient {
|
|||||||
|
|
||||||
connect(url) {
|
connect(url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.socket = new window.WebSocket(this.url);
|
this.socket = new window.WebSocket(this.formatWebSocketUrl(this.url));
|
||||||
|
|
||||||
this.socket.onopen = () => {
|
this.socket.onopen = () => {
|
||||||
console.log('WebSocket connected');
|
console.log('WebSocket connected');
|
||||||
@@ -69,8 +69,33 @@ class WebSocketClient {
|
|||||||
console.error('WebSocket is not open. Cannot send data.');
|
console.error('WebSocket is not open. Cannot send data.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formatWebSocketUrl(url) {
|
||||||
|
// Vérifier si l'URL est déjà un protocole WebSocket
|
||||||
|
if (url.startsWith('ws://') || url.startsWith('wss://')) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déterminer si on utilise le protocole sécurisé (wss) ou non (ws)
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
|
||||||
|
// Si l'URL est relative (/mon_url)
|
||||||
|
if (url.startsWith('/')) {
|
||||||
|
return `${protocol}//${window.location.host}${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si l'URL est complète (http://monsite.com/mon_url)
|
||||||
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||||
|
return url.replace(/^http(s)?:\/\//, (_, s) => s ? 'wss://' : 'ws://');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si c'est juste un chemin sans slash au début
|
||||||
|
return `${protocol}//${window.location.host}/${url}`;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
install: (app, options) => {
|
install: (app, options) => {
|
||||||
app.config.globalProperties.$ws = new WebSocketClient();
|
app.config.globalProperties.$ws = new WebSocketClient();
|
||||||
|
|||||||
26
app/frontend/src/styles/main.css
Normal file
26
app/frontend/src/styles/main.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pour Firefox */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #888 #f1f1f1;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ django-cors-headers
|
|||||||
djangorestframework
|
djangorestframework
|
||||||
django-filter
|
django-filter
|
||||||
djangorestframework-simplejwt
|
djangorestframework-simplejwt
|
||||||
channels[daphne]
|
channels
|
||||||
channels_redis
|
channels_redis
|
||||||
|
|
||||||
celery
|
celery
|
||||||
@@ -14,4 +14,7 @@ psycopg[binary]
|
|||||||
uvicorn
|
uvicorn
|
||||||
transmission-rpc
|
transmission-rpc
|
||||||
stream-zip
|
stream-zip
|
||||||
aiofiles
|
anyio
|
||||||
|
websockets
|
||||||
|
uvloop
|
||||||
|
watchfiles
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class File(models.Model):
|
|||||||
def mime_types(self):
|
def mime_types(self):
|
||||||
mime = mimetypes.guess_type(self.pathname)
|
mime = mimetypes.guess_type(self.pathname)
|
||||||
if mime:
|
if mime:
|
||||||
return mime
|
return mime[0] or mime[1] or "application/octet-stream"
|
||||||
else:
|
else:
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
|
|
||||||
@@ -87,7 +87,21 @@ class File(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def accel_redirect(self):
|
def accel_redirect(self):
|
||||||
return shlex.quote(f"{settings.NGINX_ACCEL_BASE}/{self.pathname}")
|
# Encode chaque partie du chemin séparément pour préserver la structure
|
||||||
|
encoded_parts = []
|
||||||
|
for part in self.pathname.parts:
|
||||||
|
# Ignorer un slash initial si présent
|
||||||
|
if part == '/' or part == '\\':
|
||||||
|
continue
|
||||||
|
encoded_parts.append(quote(part))
|
||||||
|
|
||||||
|
# Construction du chemin final avec le préfixe Nginx
|
||||||
|
if settings.NGINX_ACCEL_BASE.endswith('/'):
|
||||||
|
base = settings.NGINX_ACCEL_BASE.rstrip('/')
|
||||||
|
else:
|
||||||
|
base = settings.NGINX_ACCEL_BASE
|
||||||
|
|
||||||
|
return f"{base}/{'/'.join(encoded_parts)}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def disposition(self):
|
def disposition(self):
|
||||||
|
|||||||
@@ -6,4 +6,14 @@
|
|||||||
const current_user = {{ request.user.min_infos|safe }};
|
const current_user = {{ request.user.min_infos|safe }};
|
||||||
</script>
|
</script>
|
||||||
{% vite_asset "app/torrent.js" %}
|
{% vite_asset "app/torrent.js" %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function QWebInit(){
|
||||||
|
if(typeof(QWebChannel) !== 'undefined'){
|
||||||
|
new QWebChannel(qt.webChannelTransport, channel => {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import HomeView, download_file, download_torrent
|
from .views import HomeView, download_file, download_torrent, pping
|
||||||
|
|
||||||
app_name = "torrent"
|
app_name = "torrent"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", HomeView.as_view(), name="home"),
|
path("", HomeView.as_view(), name="home"),
|
||||||
|
path("pping/", pping, name="pping"),
|
||||||
path("download_file/<uuid:file_id>", download_file, name="download_file"),
|
path("download_file/<uuid:file_id>", download_file, name="download_file"),
|
||||||
path("download_torrent/<str:torrent_id>", download_torrent, name="download_torrent"),
|
path("download_torrent/<str:torrent_id>", download_torrent, name="download_torrent"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ from django.db.models import Q, Count, OuterRef
|
|||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.http import HttpResponse, Http404, StreamingHttpResponse
|
from django.http import HttpResponse, Http404, StreamingHttpResponse
|
||||||
|
|
||||||
import aiofiles
|
|
||||||
from rest_framework.viewsets import GenericViewSet
|
from rest_framework.viewsets import GenericViewSet
|
||||||
from rest_framework import mixins
|
from rest_framework import mixins
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
import anyio
|
||||||
|
|
||||||
from app.utils import StreamingZipFileResponse
|
from app.utils import StreamingZipFileResponse
|
||||||
from user.models import User
|
from user.models import User
|
||||||
@@ -23,6 +23,10 @@ class HomeView(LoginRequiredMixin, TemplateView):
|
|||||||
template_name = "torrent/home.html"
|
template_name = "torrent/home.html"
|
||||||
|
|
||||||
|
|
||||||
|
def pping(request):
|
||||||
|
return HttpResponse(str(dict(request.session)))
|
||||||
|
|
||||||
|
|
||||||
async def download_file(request, file_id):
|
async def download_file(request, file_id):
|
||||||
user = await request.auser()
|
user = await request.auser()
|
||||||
qs = File.objects.filter(
|
qs = File.objects.filter(
|
||||||
@@ -32,16 +36,16 @@ async def download_file(request, file_id):
|
|||||||
| Q(torrent__shared_users__friends=user),
|
| Q(torrent__shared_users__friends=user),
|
||||||
torrent__transmission_data__progress__gte=100,
|
torrent__transmission_data__progress__gte=100,
|
||||||
pk=file_id
|
pk=file_id
|
||||||
)
|
).distinct()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
file = await qs.aget()
|
file = await qs.aget()
|
||||||
except File.DoesNotExist:
|
except File.DoesNotExist:
|
||||||
raise Http404()
|
raise Http404()
|
||||||
else:
|
else:
|
||||||
if request.GET.get("dl_hotfix", "0") == "1":
|
if int(request.GET.get("dl_hotfix", 0)) == 1:
|
||||||
async def read_file():
|
async def read_file():
|
||||||
async with aiofiles.open(file.abs_pathname, "rb") as f:
|
async with await anyio.open_file(file.abs_pathname, "rb") as f:
|
||||||
while chunk := await f.read(128 * 1024):
|
while chunk := await f.read(128 * 1024):
|
||||||
yield chunk
|
yield chunk
|
||||||
response = StreamingHttpResponse(read_file())
|
response = StreamingHttpResponse(read_file())
|
||||||
@@ -57,6 +61,30 @@ async def download_file(request, file_id):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
async def flux_file(request, file_id):
|
||||||
|
user = await request.auser()
|
||||||
|
qs = File.objects.filter(
|
||||||
|
Q(torrent__user=user)
|
||||||
|
| Q(torrent__shared_users=user)
|
||||||
|
| Q(torrent__user__friends=user)
|
||||||
|
| Q(torrent__shared_users__friends=user),
|
||||||
|
torrent__transmission_data__progress__gte=100,
|
||||||
|
pk=file_id
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
try:
|
||||||
|
file = await qs.aget()
|
||||||
|
except File.DoesNotExist:
|
||||||
|
raise Http404()
|
||||||
|
else:
|
||||||
|
response = HttpResponse()
|
||||||
|
response["X-Accel-Redirect"] = file.accel_redirect
|
||||||
|
response["X-Accel-Buffering"] = "no"
|
||||||
|
response["Content-Type"] = file.mime_types
|
||||||
|
response["Content-Disposition"] = file.disposition
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
async def download_torrent(request, torrent_id):
|
async def download_torrent(request, torrent_id):
|
||||||
# py version
|
# py version
|
||||||
user = await request.auser()
|
user = await request.auser()
|
||||||
@@ -67,11 +95,11 @@ async def download_torrent(request, torrent_id):
|
|||||||
| Q(shared_users__friends=user),
|
| Q(shared_users__friends=user),
|
||||||
transmission_data__progress__gte=100,
|
transmission_data__progress__gte=100,
|
||||||
pk=torrent_id
|
pk=torrent_id
|
||||||
).annotate(count_files=Count("files"))
|
).annotate(count_files=Count("files")).distinct()
|
||||||
|
|
||||||
torrent = await qs.aget()
|
torrent = await qs.aget()
|
||||||
|
|
||||||
if torrent.count_files == 1:
|
if await torrent.alen_files == 1:
|
||||||
file = await torrent.files.afirst()
|
file = await torrent.files.afirst()
|
||||||
return redirect(reverse("torrent:download_file", kwargs={
|
return redirect(reverse("torrent:download_file", kwargs={
|
||||||
"file_id": file.pk
|
"file_id": file.pk
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ services:
|
|||||||
&& cd frontend && yarn build && cd ..
|
&& cd frontend && yarn build && cd ..
|
||||||
&& python manage.py collectstatic --noinput
|
&& python manage.py collectstatic --noinput
|
||||||
&& python manage.py migrate
|
&& python manage.py migrate
|
||||||
&& uvicorn app.asgi:application --workers 3 --host 0.0.0.0 --port 8000 --lifespan off --loop asyncio --ws websockets"
|
&& uvicorn app.asgi:application --workers 3 --host 0.0.0.0 --port 8000 --lifespan off --loop uvloop --ws websockets"
|
||||||
|
|
||||||
# celery:
|
# celery:
|
||||||
# extends:
|
# extends:
|
||||||
|
|||||||
Reference in New Issue
Block a user